mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-07-24 17:35:43 +02:00
sidepanel: improve smartlist interface with underlying models
Minor cosmetic changes to the account combo box, search bar, filter tabs, and smartlist. Change-Id: Ie8173504859b325374e42f0dbb4e0ae75f3ed740 Gitlab: #373 Gitlab: #374 Gitlab: #388
This commit is contained in:
parent
0d6d94d124
commit
e64a9e7ee7
56 changed files with 1998 additions and 1346 deletions
|
@ -67,7 +67,11 @@ set(COMMON_SOURCES
|
|||
${SRC_DIR}/screensaver.cpp
|
||||
${SRC_DIR}/systemtray.cpp
|
||||
${SRC_DIR}/appsettingsmanager.cpp
|
||||
${SRC_DIR}/lrcinstance.cpp)
|
||||
${SRC_DIR}/lrcinstance.cpp
|
||||
${SRC_DIR}/selectablelistproxymodel.cpp
|
||||
${SRC_DIR}/conversationlistmodelbase.cpp
|
||||
${SRC_DIR}/conversationlistmodel.cpp
|
||||
${SRC_DIR}/searchresultslistmodel.cpp)
|
||||
|
||||
set(COMMON_HEADERS
|
||||
${SRC_DIR}/avatarimageprovider.h
|
||||
|
@ -118,7 +122,11 @@ set(COMMON_HEADERS
|
|||
${SRC_DIR}/screensaver.h
|
||||
${SRC_DIR}/systemtray.h
|
||||
${SRC_DIR}/appsettingsmanager.h
|
||||
${SRC_DIR}/lrcinstance.h)
|
||||
${SRC_DIR}/lrcinstance.h
|
||||
${SRC_DIR}/selectablelistproxymodel.h
|
||||
${SRC_DIR}/conversationlistmodelbase.h
|
||||
${SRC_DIR}/conversationlistmodel.h
|
||||
${SRC_DIR}/searchresultslistmodel.h)
|
||||
|
||||
set(QML_LIBS
|
||||
Qt5::Quick
|
||||
|
|
5
qml.qrc
5
qml.qrc
|
@ -102,7 +102,6 @@
|
|||
<file>src/mainview/components/MessageWebView.qml</file>
|
||||
<file>src/mainview/components/MessageWebViewHeader.qml</file>
|
||||
<file>src/mainview/components/AccountComboBox.qml</file>
|
||||
<file>src/mainview/components/ConversationSmartListView.qml</file>
|
||||
<file>src/mainview/components/CallStackView.qml</file>
|
||||
<file>src/mainview/components/IncomingCallPage.qml</file>
|
||||
<file>src/mainview/components/OutgoingCallPage.qml</file>
|
||||
|
@ -114,7 +113,6 @@
|
|||
<file>src/mainview/components/ParticipantOverlay.qml</file>
|
||||
<file>src/mainview/components/ProjectCreditsScrollView.qml</file>
|
||||
<file>src/mainview/components/AccountComboBoxPopup.qml</file>
|
||||
<file>src/mainview/components/ConversationSmartListViewItemDelegate.qml</file>
|
||||
<file>src/mainview/components/SidePanelTabBar.qml</file>
|
||||
<file>src/mainview/components/WelcomePageQrDialog.qml</file>
|
||||
<file>src/mainview/components/ConversationSmartListContextMenu.qml</file>
|
||||
|
@ -138,5 +136,8 @@
|
|||
<file>src/mainview/js/pluginhandlerpickercreation.js</file>
|
||||
<file>src/mainview/components/FilterTabButton.qml</file>
|
||||
<file>src/mainview/components/AccountItemDelegate.qml</file>
|
||||
<file>src/mainview/components/ConversationListView.qml</file>
|
||||
<file>src/mainview/components/SmartListItemDelegate.qml</file>
|
||||
<file>src/mainview/components/BadgeNotifier.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
|
|
@ -43,8 +43,6 @@ AccountAdapter::safeInit()
|
|||
this,
|
||||
&AccountAdapter::onCurrentAccountChanged);
|
||||
|
||||
deselectConversation();
|
||||
|
||||
auto accountId = lrcInstance_->getCurrAccId();
|
||||
setProperties(accountId);
|
||||
connectAccount(accountId);
|
||||
|
@ -65,7 +63,6 @@ AccountAdapter::getDeviceModel()
|
|||
void
|
||||
AccountAdapter::changeAccount(int row)
|
||||
{
|
||||
deselectConversation(); // Hack UI
|
||||
auto accountList = lrcInstance_->accountModel().getAccountList();
|
||||
if (accountList.size() > row) {
|
||||
lrcInstance_->setSelectedAccountId(accountList.at(row));
|
||||
|
@ -269,12 +266,6 @@ AccountAdapter::setCurrAccDisplayName(const QString& text)
|
|||
lrcInstance_->setCurrAccDisplayName(text);
|
||||
}
|
||||
|
||||
void
|
||||
AccountAdapter::setSelectedConvId(const QString& convId)
|
||||
{
|
||||
lrcInstance_->set_selectedConvUid(convId);
|
||||
}
|
||||
|
||||
lrc::api::profile::Type
|
||||
AccountAdapter::getCurrentAccountType()
|
||||
{
|
||||
|
@ -347,23 +338,6 @@ AccountAdapter::passwordSetStatusMessageBox(bool success, QString title, QString
|
|||
}
|
||||
}
|
||||
|
||||
void
|
||||
AccountAdapter::deselectConversation()
|
||||
{
|
||||
if (lrcInstance_->get_selectedConvUid().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: remove this unhealthy section
|
||||
auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
|
||||
|
||||
if (currentConversationModel == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
lrcInstance_->set_selectedConvUid();
|
||||
}
|
||||
|
||||
void
|
||||
AccountAdapter::connectAccount(const QString& accountId)
|
||||
{
|
||||
|
@ -374,7 +348,7 @@ AccountAdapter::connectAccount(const QString& accountId)
|
|||
QObject::disconnect(accountProfileUpdatedConnection_);
|
||||
QObject::disconnect(contactAddedConnection_);
|
||||
QObject::disconnect(addedToConferenceConnection_);
|
||||
QObject::disconnect(contactUnbannedConnection_);
|
||||
QObject::disconnect(bannedStatusChangedConnection_);
|
||||
|
||||
accountStatusChangedConnection_
|
||||
= QObject::connect(accInfo.accountModel,
|
||||
|
@ -390,27 +364,22 @@ AccountAdapter::connectAccount(const QString& accountId)
|
|||
Q_EMIT accountStatusChanged(accountId);
|
||||
});
|
||||
|
||||
contactAddedConnection_
|
||||
= QObject::connect(accInfo.contactModel.get(),
|
||||
&lrc::api::ContactModel::contactAdded,
|
||||
[this, accountId](const QString& contactUri) {
|
||||
const auto& convInfo = lrcInstance_->getConversationFromConvUid(
|
||||
lrcInstance_->get_selectedConvUid());
|
||||
if (convInfo.uid.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto& accInfo = lrcInstance_->accountModel().getAccountInfo(
|
||||
accountId);
|
||||
if (contactUri
|
||||
== accInfo.contactModel
|
||||
->getContact(convInfo.participants.at(0))
|
||||
.profileInfo.uri) {
|
||||
/*
|
||||
* Update conversation.
|
||||
*/
|
||||
Q_EMIT updateConversationForAddedContact();
|
||||
}
|
||||
});
|
||||
contactAddedConnection_ = QObject::connect(
|
||||
accInfo.contactModel.get(),
|
||||
&lrc::api::ContactModel::contactAdded,
|
||||
[this, accountId](const QString& contactUri) {
|
||||
const auto& convInfo = lrcInstance_->getConversationFromConvUid(
|
||||
lrcInstance_->get_selectedConvUid());
|
||||
if (convInfo.uid.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto& accInfo = lrcInstance_->accountModel().getAccountInfo(accountId);
|
||||
auto selectedContactUri
|
||||
= accInfo.contactModel->getContact(convInfo.participants.at(0)).profileInfo.uri;
|
||||
if (contactUri == selectedContactUri) {
|
||||
Q_EMIT selectedContactAdded(convInfo.uid);
|
||||
}
|
||||
});
|
||||
|
||||
addedToConferenceConnection_
|
||||
= QObject::connect(accInfo.callModel.get(),
|
||||
|
@ -420,12 +389,15 @@ AccountAdapter::connectAccount(const QString& accountId)
|
|||
lrcInstance_->renderer()->addDistantRenderer(confId);
|
||||
});
|
||||
|
||||
contactUnbannedConnection_ = QObject::connect(accInfo.contactModel.get(),
|
||||
&lrc::api::ContactModel::bannedStatusChanged,
|
||||
[this](const QString&, bool banned) {
|
||||
if (!banned)
|
||||
Q_EMIT contactUnbanned();
|
||||
});
|
||||
bannedStatusChangedConnection_
|
||||
= QObject::connect(accInfo.contactModel.get(),
|
||||
&lrc::api::ContactModel::bannedStatusChanged,
|
||||
[this](const QString& uri, bool banned) {
|
||||
if (!banned)
|
||||
Q_EMIT contactUnbanned();
|
||||
else
|
||||
Q_EMIT lrcInstance_->contactBanned(uri);
|
||||
});
|
||||
} catch (...) {
|
||||
qWarning() << "Couldn't get account: " << accountId;
|
||||
}
|
||||
|
|
|
@ -95,16 +95,14 @@ public:
|
|||
Q_INVOKABLE bool hasVideoCall();
|
||||
Q_INVOKABLE bool isPreviewing();
|
||||
Q_INVOKABLE void setCurrAccDisplayName(const QString& text);
|
||||
Q_INVOKABLE void setSelectedConvId(const QString& convId = {});
|
||||
Q_INVOKABLE lrc::api::profile::Type getCurrentAccountType();
|
||||
|
||||
Q_INVOKABLE void setCurrAccAvatar(bool fromFile, const QString& source);
|
||||
|
||||
Q_SIGNALS:
|
||||
// Trigger other components to reconnect account related signals.
|
||||
void accountStatusChanged(QString accountId = {});
|
||||
|
||||
void updateConversationForAddedContact();
|
||||
void accountStatusChanged(QString accountId);
|
||||
void selectedContactAdded(QString convId);
|
||||
|
||||
// Send report failure to QML to make it show the right UI state .
|
||||
void reportFailure();
|
||||
|
@ -119,8 +117,6 @@ private:
|
|||
lrc::api::profile::Type currentAccountType_ {};
|
||||
int accountListSize_ {};
|
||||
|
||||
void deselectConversation();
|
||||
|
||||
// Make account signal connections.
|
||||
void connectAccount(const QString& accountId);
|
||||
|
||||
|
@ -134,7 +130,7 @@ private:
|
|||
QMetaObject::Connection accountProfileUpdatedConnection_;
|
||||
QMetaObject::Connection contactAddedConnection_;
|
||||
QMetaObject::Connection addedToConferenceConnection_;
|
||||
QMetaObject::Connection contactUnbannedConnection_;
|
||||
QMetaObject::Connection bannedStatusChangedConnection_;
|
||||
QMetaObject::Connection registeredNameSavedConnection_;
|
||||
|
||||
AppSettingsManager* settingsManager_;
|
||||
|
|
|
@ -59,9 +59,9 @@ CallAdapter::CallAdapter(SystemTray* systemTray, LRCInstance* instance, QObject*
|
|||
[this](const QString& accountId, const QString& convUid) {
|
||||
acceptACall(accountId, convUid);
|
||||
Q_EMIT lrcInstance_->notificationClicked();
|
||||
lrcInstance_->selectConversation(accountId, convUid);
|
||||
lrcInstance_->selectConversation(convUid, accountId);
|
||||
updateCall(convUid, accountId);
|
||||
Q_EMIT callSetupMainViewRequired(accountId, convUid);
|
||||
Q_EMIT lrcInstance_->conversationUpdated(convUid, accountId);
|
||||
});
|
||||
connect(systemTray_,
|
||||
&SystemTray::declineCallActivated,
|
||||
|
@ -200,6 +200,7 @@ CallAdapter::onShowIncomingCallView(const QString& accountId, const QString& con
|
|||
auto selectedAccountId = lrcInstance_->getCurrAccId();
|
||||
auto* callModel = lrcInstance_->getCurrentCallModel();
|
||||
|
||||
// new call
|
||||
if (!callModel->hasCall(convInfo.callId)) {
|
||||
if (QApplication::focusObject() == nullptr || accountId != selectedAccountId) {
|
||||
showNotification(accountId, convInfo.uid);
|
||||
|
@ -219,17 +220,21 @@ CallAdapter::onShowIncomingCallView(const QString& accountId, const QString& con
|
|||
return;
|
||||
}
|
||||
}
|
||||
Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
|
||||
Q_EMIT lrcInstance_->updateSmartList();
|
||||
// select
|
||||
lrcInstance_->selectConversation(convInfo.uid, accountId);
|
||||
return;
|
||||
}
|
||||
|
||||
// this slot has been triggered as a result of either selecting a conversation
|
||||
// with an active call, placing a call, or an incoming call for the current
|
||||
// or any other conversation
|
||||
auto call = callModel->getCall(convInfo.callId);
|
||||
auto isCallSelected = lrcInstance_->get_selectedConvUid() == convInfo.uid;
|
||||
|
||||
if (call.isOutgoing) {
|
||||
if (isCallSelected) {
|
||||
Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
|
||||
// don't reselect
|
||||
Q_EMIT lrcInstance_->conversationUpdated(convInfo.uid, accountId);
|
||||
}
|
||||
} else {
|
||||
auto accountProperties = lrcInstance_->accountModel().getAccountConfig(selectedAccountId);
|
||||
|
@ -254,10 +259,12 @@ CallAdapter::onShowIncomingCallView(const QString& accountId, const QString& con
|
|||
showNotification(accountId, convInfo.uid);
|
||||
return;
|
||||
} else {
|
||||
Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
|
||||
// only update
|
||||
Q_EMIT lrcInstance_->conversationUpdated(convInfo.uid, accountId);
|
||||
}
|
||||
} else {
|
||||
Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
|
||||
// only update
|
||||
Q_EMIT lrcInstance_->conversationUpdated(convInfo.uid, accountId);
|
||||
}
|
||||
} else { // Not current conversation
|
||||
if (currentConvHasCall) {
|
||||
|
@ -269,19 +276,19 @@ CallAdapter::onShowIncomingCallView(const QString& accountId, const QString& con
|
|||
return;
|
||||
}
|
||||
}
|
||||
Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
|
||||
// reselect
|
||||
lrcInstance_->selectConversation(convInfo.uid, accountId);
|
||||
}
|
||||
}
|
||||
}
|
||||
Q_EMIT callStatusChanged(static_cast<int>(call.status), accountId, convInfo.uid);
|
||||
Q_EMIT lrcInstance_->updateSmartList();
|
||||
}
|
||||
|
||||
void
|
||||
CallAdapter::onShowCallView(const QString& accountId, const QString& convUid)
|
||||
{
|
||||
updateCall(convUid, accountId);
|
||||
Q_EMIT callSetupMainViewRequired(accountId, convUid);
|
||||
Q_EMIT lrcInstance_->conversationUpdated(convUid, accountId);
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -399,8 +406,6 @@ CallAdapter::showNotification(const QString& accountId, const QString& convUid)
|
|||
from = accInfo.contactModel->bestNameForContact(convInfo.participants[0]);
|
||||
}
|
||||
|
||||
Q_EMIT lrcInstance_->updateSmartList();
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
auto contactPhoto = Utils::contactPhoto(lrcInstance_,
|
||||
convInfo.participants[0],
|
||||
|
@ -414,12 +419,11 @@ CallAdapter::showNotification(const QString& accountId, const QString& convUid)
|
|||
Utils::QImageToByteArray(contactPhoto));
|
||||
#else
|
||||
auto onClicked = [this, accountId, convUid = convInfo.uid]() {
|
||||
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid, accountId);
|
||||
if (convInfo.uid.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Q_EMIT lrcInstance_->notificationClicked();
|
||||
Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
|
||||
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid, accountId);
|
||||
if (convInfo.uid.isEmpty())
|
||||
return;
|
||||
lrcInstance_->selectConversation(convInfo.uid, accountId);
|
||||
};
|
||||
systemTray_->showNotification(tr("is calling you"), from, onClicked);
|
||||
#endif
|
||||
|
@ -484,7 +488,7 @@ CallAdapter::connectCallModel(const QString& accountId)
|
|||
case lrc::api::call::Status::TIMEOUT:
|
||||
case lrc::api::call::Status::TERMINATING: {
|
||||
lrcInstance_->renderer()->removeDistantRenderer(callId);
|
||||
Q_EMIT callSetupMainViewRequired(accountId, convInfo.uid);
|
||||
Q_EMIT lrcInstance_->conversationUpdated(convInfo.uid, accountId);
|
||||
if (convInfo.uid.isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
@ -517,7 +521,7 @@ CallAdapter::connectCallModel(const QString& accountId)
|
|||
/*
|
||||
* Reset the call view corresponding accountId, uid.
|
||||
*/
|
||||
lrcInstance_->set_selectedConvUid(otherConv.uid);
|
||||
lrcInstance_->selectConversation(otherConv.uid);
|
||||
updateCall(otherConv.uid, otherConv.accountId, forceCallOnly);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,11 +80,9 @@ public:
|
|||
|
||||
Q_SIGNALS:
|
||||
void callStatusChanged(int index, const QString& accountId, const QString& convUid);
|
||||
void updateConversationSmartList();
|
||||
void updateParticipantsInfos(const QVariantList& infos,
|
||||
const QString& accountId,
|
||||
const QString& callId);
|
||||
void callSetupMainViewRequired(const QString& accountId, const QString& convUid);
|
||||
void previewVisibilityNeedToChange(bool visible);
|
||||
|
||||
// For Call Overlay
|
||||
|
|
|
@ -38,6 +38,7 @@ Item {
|
|||
|
||||
property alias fillMode: rootImage.fillMode
|
||||
property alias sourceSize: rootImage.sourceSize
|
||||
property int transitionDuration: 150
|
||||
property bool saveToConfig: false
|
||||
property int mode: AvatarImage.Mode.FromAccount
|
||||
property string imageProviderIdPrefix: {
|
||||
|
@ -178,7 +179,7 @@ Item {
|
|||
NumberAnimation {
|
||||
properties: "opacity"
|
||||
easing.type: Easing.InOutQuad
|
||||
duration: 400
|
||||
duration: transitionDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -188,38 +189,15 @@ Item {
|
|||
id: presenceIndicator
|
||||
|
||||
anchors.right: root.right
|
||||
anchors.rightMargin: -1
|
||||
anchors.bottom: root.bottom
|
||||
anchors.bottomMargin: -1
|
||||
|
||||
size: root.width * 0.3
|
||||
size: root.width * 0.26
|
||||
|
||||
visible: showPresenceIndicator
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: unreadMessageCountRect
|
||||
|
||||
anchors.right: root.right
|
||||
anchors.top: root.top
|
||||
|
||||
width: root.width * 0.3
|
||||
height: root.width * 0.3
|
||||
|
||||
visible: unreadMessagesCount > 0
|
||||
|
||||
Text {
|
||||
id: unreadMessageCounttext
|
||||
|
||||
anchors.centerIn: unreadMessageCountRect
|
||||
|
||||
text: unreadMessagesCount > 9 ? "…" : unreadMessagesCount
|
||||
color: "white"
|
||||
font.pointSize: JamiTheme.indicatorFontSize
|
||||
}
|
||||
|
||||
radius: 30
|
||||
color: JamiTheme.notificationRed
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ScreenInfo
|
||||
|
||||
|
|
|
@ -31,6 +31,12 @@ Menu {
|
|||
property int commonBorderWidth: 1
|
||||
font.pointSize: JamiTheme.menuFontSize
|
||||
|
||||
modal: true
|
||||
Overlay.modal: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
// TODO: investigate
|
||||
function openMenu(){
|
||||
visible = true
|
||||
visible = false
|
||||
|
@ -38,6 +44,8 @@ Menu {
|
|||
}
|
||||
|
||||
background: Rectangle {
|
||||
id: container
|
||||
|
||||
implicitWidth: menuItemsPreferredWidth
|
||||
implicitHeight: menuItemsPreferredHeight
|
||||
* (root.count - generalMenuSeparatorCount)
|
||||
|
@ -45,5 +53,15 @@ Menu {
|
|||
border.width: commonBorderWidth
|
||||
border.color: JamiTheme.tabbarBorderColor
|
||||
color: JamiTheme.backgroundColor
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: DropShadow {
|
||||
z: -1
|
||||
horizontalOffset: 3.0
|
||||
verticalOffset: 3.0
|
||||
radius: 16.0
|
||||
samples: 16
|
||||
color: JamiTheme.shadowColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ Popup {
|
|||
height: root.height
|
||||
horizontalOffset: 3.0
|
||||
verticalOffset: 3.0
|
||||
radius: container.radius * 2
|
||||
radius: container.radius * 4
|
||||
samples: 16
|
||||
color: JamiTheme.shadowColor
|
||||
source: container
|
||||
|
|
|
@ -30,7 +30,7 @@ Rectangle {
|
|||
// This is set to REGISTERED for contact presence
|
||||
// as status is not currently tracked for contact items.
|
||||
property int status: Account.Status.REGISTERED
|
||||
property int size: 12
|
||||
property int size: 15
|
||||
|
||||
width: size
|
||||
height: size
|
||||
|
|
|
@ -23,6 +23,12 @@ import QtQuick.Controls 2.12
|
|||
Rectangle {
|
||||
property alias name: label.text
|
||||
property bool stretchParent: false
|
||||
property string tag: this.toString()
|
||||
signal moveX(real dx)
|
||||
signal moveY(real dy)
|
||||
property real ox: 0
|
||||
property real oy: 0
|
||||
property real step: 0.5
|
||||
|
||||
border.width: 1
|
||||
color: {
|
||||
|
@ -33,6 +39,19 @@ Rectangle {
|
|||
}
|
||||
anchors.fill: parent
|
||||
focus: false
|
||||
Keys.onPressed: {
|
||||
if (event.key === Qt.Key_Left)
|
||||
moveX(-step)
|
||||
else if (event.key === Qt.Key_Right)
|
||||
moveX(step)
|
||||
else if (event.key === Qt.Key_Down)
|
||||
moveY(step)
|
||||
else if (event.key === Qt.Key_Up)
|
||||
moveY(-step)
|
||||
console.log(tag, ox, oy)
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// fallback to some description of the object
|
||||
if (label.text === "")
|
||||
|
@ -45,10 +64,24 @@ Rectangle {
|
|||
}
|
||||
}
|
||||
|
||||
onMoveX: {
|
||||
parent.anchors.leftMargin += dx
|
||||
parent.x += dx
|
||||
ox += dx;
|
||||
}
|
||||
onMoveY: {
|
||||
parent.anchors.topMargin += dy
|
||||
parent.y += dy
|
||||
oy += dy
|
||||
}
|
||||
|
||||
Label {
|
||||
id: label
|
||||
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: parent.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -330,9 +330,6 @@ Item {
|
|||
"Use the \"Link Another Device\" feature to obtain a PIN.")
|
||||
property string connectFromAnotherDevice: qsTr("Link device")
|
||||
|
||||
// KeyBoardShortcutTable
|
||||
property string conversations: qsTr("Conversations")
|
||||
|
||||
// LinkDevicesDialog
|
||||
property string pinTimerInfos: qsTr("The PIN and the account password should be entered in your device within 10 minutes.")
|
||||
property string close: qsTr("Close")
|
||||
|
@ -405,6 +402,8 @@ Item {
|
|||
|
||||
// SmartList
|
||||
property string clearText: qsTr("Clear Text")
|
||||
property string conversations: qsTr("Conversations")
|
||||
property string searchResults: qsTr("Search Results")
|
||||
|
||||
// SmartList context menu
|
||||
property string declineContactRequest: qsTr("Decline contact request")
|
||||
|
|
|
@ -55,10 +55,10 @@ Item {
|
|||
property color notificationBlue: "#31b7ff"
|
||||
property color unPresenceOrange: "orange"
|
||||
property color placeHolderTextFontColor: "#767676"
|
||||
property color draftRed: "#cf5300"
|
||||
property color draftTextColor: "#cf5300"
|
||||
property color selectedTabColor: primaryForegroundColor
|
||||
property color filterBadgeColor: mediumGrey
|
||||
property color filterBadgeTextColor: blackColor
|
||||
property color filterBadgeColor: "#eed4d8"
|
||||
property color filterBadgeTextColor: "#cc0022"
|
||||
|
||||
// General buttons
|
||||
property color pressedButtonColor: darkTheme ? pressColor : "#a0a0a0"
|
||||
|
@ -174,10 +174,14 @@ Item {
|
|||
property real titleFontSize: 16
|
||||
property real primaryRadius: 4
|
||||
property real smartlistItemFontSize: 10.5
|
||||
property real smartlistItemInfoFontSize: 9
|
||||
property real filterItemFontSize: smartlistItemFontSize
|
||||
property real filterBadgeFontSize: 8.25
|
||||
property real accountListItemHeight: 64
|
||||
property real accountListAvatarSize: 40
|
||||
property real smartListItemHeight: 64
|
||||
property real smartListAvatarSize: 52
|
||||
property real smartListTransitionDuration: 120
|
||||
|
||||
property real maximumWidthSettingsView: 600
|
||||
property real settingsHeaderpreferredHeight: 64
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*!
|
||||
/*
|
||||
* Copyright (C) 2020 by Savoir-faire Linux
|
||||
* Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
|
@ -48,13 +48,13 @@ ContactAdapter::getContactSelectableModel(int type)
|
|||
switch (listModeltype_) {
|
||||
case SmartListModel::Type::CONVERSATION:
|
||||
selectableProxyModel_->setPredicate([this](const QModelIndex& index, const QRegExp&) {
|
||||
return !defaultModerators_.contains(index.data(SmartListModel::URI).toString());
|
||||
return !defaultModerators_.contains(index.data(Role::URI).toString());
|
||||
});
|
||||
break;
|
||||
|
||||
case SmartListModel::Type::CONFERENCE:
|
||||
selectableProxyModel_->setPredicate([](const QModelIndex& index, const QRegExp&) {
|
||||
return index.data(SmartListModel::Presence).toBool();
|
||||
return index.data(Role::Presence).toBool();
|
||||
});
|
||||
break;
|
||||
case SmartListModel::Type::TRANSFER:
|
||||
|
@ -68,13 +68,11 @@ ContactAdapter::getContactSelectableModel(int type)
|
|||
.contactModel->bestIdForContact(conv.participants[0]);
|
||||
|
||||
QRegExp matchExcept = QRegExp(QString("\\b(?!" + calleeDisplayId + "\\b)\\w+"));
|
||||
match = matchExcept.indexIn(index.data(SmartListModel::Role::DisplayID).toString())
|
||||
!= -1;
|
||||
match = matchExcept.indexIn(index.data(Role::BestId).toString()) != -1;
|
||||
}
|
||||
|
||||
if (match) {
|
||||
match = regexp.indexIn(index.data(SmartListModel::Role::DisplayID).toString())
|
||||
!= -1;
|
||||
match = regexp.indexIn(index.data(Role::BestId).toString()) != -1;
|
||||
}
|
||||
return match && !index.parent().isValid();
|
||||
});
|
||||
|
@ -95,8 +93,8 @@ ContactAdapter::setSearchFilter(const QString& filter)
|
|||
} else if (listModeltype_ == SmartListModel::Type::CONVERSATION) {
|
||||
selectableProxyModel_->setPredicate(
|
||||
[this, filter](const QModelIndex& index, const QRegExp&) {
|
||||
return (!defaultModerators_.contains(index.data(SmartListModel::URI).toString())
|
||||
&& index.data(SmartListModel::DisplayName).toString().contains(filter));
|
||||
return (!defaultModerators_.contains(index.data(Role::URI).toString())
|
||||
&& index.data(Role::BestName).toString().contains(filter));
|
||||
});
|
||||
}
|
||||
selectableProxyModel_->setFilterRegExp(
|
||||
|
@ -114,15 +112,14 @@ ContactAdapter::contactSelected(int index)
|
|||
switch (listModeltype_) {
|
||||
case SmartListModel::Type::CONFERENCE: {
|
||||
// Conference.
|
||||
const auto sectionName = contactIndex.data(SmartListModel::Role::SectionName)
|
||||
.value<QString>();
|
||||
const auto sectionName = contactIndex.data(Role::SectionName).value<QString>();
|
||||
if (!sectionName.isEmpty()) {
|
||||
smartListModel_->toggleSection(sectionName);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto convUid = contactIndex.data(SmartListModel::Role::UID).value<QString>();
|
||||
const auto accId = contactIndex.data(SmartListModel::Role::AccountId).value<QString>();
|
||||
const auto convUid = contactIndex.data(Role::UID).value<QString>();
|
||||
const auto accId = contactIndex.data(Role::AccountId).value<QString>();
|
||||
const auto callId = lrcInstance_->getCallIdForConversationUid(convUid, accId);
|
||||
|
||||
if (!callId.isEmpty()) {
|
||||
|
@ -133,7 +130,7 @@ ContactAdapter::contactSelected(int index)
|
|||
|
||||
callModel->joinCalls(thisCallId, callId);
|
||||
} else {
|
||||
const auto contactUri = contactIndex.data(SmartListModel::Role::URI).value<QString>();
|
||||
const auto contactUri = contactIndex.data(Role::URI).value<QString>();
|
||||
auto call = lrcInstance_->getCallInfoForConversation(convInfo);
|
||||
if (!call) {
|
||||
return;
|
||||
|
@ -143,7 +140,7 @@ ContactAdapter::contactSelected(int index)
|
|||
} break;
|
||||
case SmartListModel::Type::TRANSFER: {
|
||||
// SIP Transfer.
|
||||
const auto contactUri = contactIndex.data(SmartListModel::Role::URI).value<QString>();
|
||||
const auto contactUri = contactIndex.data(Role::URI).value<QString>();
|
||||
|
||||
if (convInfo.uid.isEmpty()) {
|
||||
return;
|
||||
|
@ -170,7 +167,7 @@ ContactAdapter::contactSelected(int index)
|
|||
}
|
||||
} break;
|
||||
case SmartListModel::Type::CONVERSATION: {
|
||||
const auto contactUri = contactIndex.data(SmartListModel::Role::URI).value<QString>();
|
||||
const auto contactUri = contactIndex.data(Role::URI).value<QString>();
|
||||
if (contactUri.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*!
|
||||
/*
|
||||
* Copyright (C) 2020 by Savoir-faire Linux
|
||||
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
|
||||
*
|
||||
|
@ -20,6 +20,7 @@
|
|||
|
||||
#include "qmladapterbase.h"
|
||||
#include "smartlistmodel.h"
|
||||
#include "conversationlistmodel.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
@ -38,30 +39,42 @@ class LRCInstance;
|
|||
*/
|
||||
class SelectableProxyModel final : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
using FilterPredicate = std::function<bool(const QModelIndex&, const QRegExp&)>;
|
||||
|
||||
explicit SelectableProxyModel(QAbstractItemModel* parent)
|
||||
explicit SelectableProxyModel(QAbstractListModel* parent = nullptr)
|
||||
: QSortFilterProxyModel(parent)
|
||||
{
|
||||
setSourceModel(parent);
|
||||
setSortRole(ConversationList::Role::LastInteractionTimeStamp);
|
||||
sort(0, Qt::DescendingOrder);
|
||||
setFilterCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive);
|
||||
}
|
||||
~SelectableProxyModel() {}
|
||||
|
||||
void setPredicate(FilterPredicate filterPredicate)
|
||||
{
|
||||
filterPredicate_ = filterPredicate;
|
||||
}
|
||||
|
||||
virtual bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const
|
||||
virtual bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override
|
||||
{
|
||||
// Accept all contacts in conversation list filtered with account type, except those in a call.
|
||||
auto index = sourceModel()->index(source_row, 0, source_parent);
|
||||
auto index = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||
return filterPredicate_ ? filterPredicate_(index, filterRegExp()) : false;
|
||||
}
|
||||
|
||||
bool lessThan(const QModelIndex& left, const QModelIndex& right) const override
|
||||
{
|
||||
QVariant leftData = sourceModel()->data(left, sortRole());
|
||||
QVariant rightData = sourceModel()->data(right, sortRole());
|
||||
// we're assuming the sort role data type here is some integral time
|
||||
return leftData.toUInt() < rightData.toUInt();
|
||||
};
|
||||
|
||||
private:
|
||||
std::function<bool(const QModelIndex&, const QRegExp&)> filterPredicate_;
|
||||
FilterPredicate filterPredicate_;
|
||||
};
|
||||
|
||||
class ContactAdapter final : public QmlAdapterBase
|
||||
|
@ -73,6 +86,8 @@ public:
|
|||
~ContactAdapter() = default;
|
||||
|
||||
protected:
|
||||
using Role = ConversationList::Role;
|
||||
|
||||
void safeInit() override {};
|
||||
|
||||
Q_INVOKABLE QVariant getContactSelectableModel(int type);
|
||||
|
@ -81,10 +96,8 @@ protected:
|
|||
|
||||
private:
|
||||
SmartListModel::Type listModeltype_;
|
||||
|
||||
// SmartListModel is the source model of SelectableProxyModel.
|
||||
std::unique_ptr<SmartListModel> smartListModel_;
|
||||
std::unique_ptr<SelectableProxyModel> selectableProxyModel_;
|
||||
QScopedPointer<SmartListModel> smartListModel_;
|
||||
QScopedPointer<SelectableProxyModel> selectableProxyModel_;
|
||||
|
||||
QStringList defaultModerators_;
|
||||
|
||||
|
|
131
src/conversationlistmodel.cpp
Normal file
131
src/conversationlistmodel.cpp
Normal 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();
|
||||
};
|
55
src/conversationlistmodel.h
Normal file
55
src/conversationlistmodel.h
Normal 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_;
|
||||
};
|
201
src/conversationlistmodelbase.cpp
Normal file
201
src/conversationlistmodelbase.cpp
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
88
src/conversationlistmodelbase.h
Normal file
88
src/conversationlistmodelbase.h
Normal 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_;
|
||||
};
|
|
@ -1,11 +1,7 @@
|
|||
/*!
|
||||
/*
|
||||
* Copyright (C) 2020 by Savoir-faire Linux
|
||||
* Author: Edric Ladent Milaret <edric.ladent-milaret@savoirfairelinux.com>
|
||||
* Author: Anthony Léonard <anthony.leonard@savoirfairelinux.com>
|
||||
* Author: Olivier Soldano <olivier.soldano@savoirfairelinux.com>
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
* Author: Isa Nanic <isa.nanic@savoirfairelinux.com>
|
||||
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -26,35 +22,92 @@
|
|||
#include "utils.h"
|
||||
#include "qtutils.h"
|
||||
#include "systemtray.h"
|
||||
#include "qmlregister.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QJsonObject>
|
||||
|
||||
using namespace lrc::api;
|
||||
|
||||
ConversationsAdapter::ConversationsAdapter(SystemTray* systemTray,
|
||||
LRCInstance* instance,
|
||||
QObject* parent)
|
||||
: QmlAdapterBase(instance, parent)
|
||||
, currentTypeFilter_(profile::Type::RING)
|
||||
, systemTray_(systemTray)
|
||||
, convSrcModel_(new ConversationListModel(lrcInstance_))
|
||||
, convModel_(new ConversationListProxyModel(convSrcModel_.get()))
|
||||
, searchSrcModel_(new SearchResultsListModel(lrcInstance_))
|
||||
, searchModel_(new SelectableListProxyModel(searchSrcModel_.get()))
|
||||
{
|
||||
QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, convModel_.get(), "ConversationListModel");
|
||||
QML_REGISTERSINGLETONTYPE_POBJECT(NS_MODELS, searchModel_.get(), "SearchResultsListModel");
|
||||
|
||||
new SelectableListProxyGroupModel({convModel_.data(), searchModel_.data()}, this);
|
||||
|
||||
setTypeFilter(currentTypeFilter_);
|
||||
connect(this, &ConversationsAdapter::currentTypeFilterChanged, [this]() {
|
||||
lrcInstance_->getCurrentConversationModel()->setFilter(currentTypeFilter_);
|
||||
setTypeFilter(currentTypeFilter_);
|
||||
});
|
||||
|
||||
connect(lrcInstance_, &LRCInstance::conversationSelected, [this]() {
|
||||
auto convUid = lrcInstance_->get_selectedConvUid();
|
||||
if (!convUid.isEmpty()) {
|
||||
Q_EMIT showConversation(lrcInstance_->getCurrAccId(), convUid);
|
||||
connect(lrcInstance_, &LRCInstance::selectedConvUidChanged, [this]() {
|
||||
auto convId = lrcInstance_->get_selectedConvUid();
|
||||
if (convId.isEmpty()) {
|
||||
// deselected
|
||||
convModel_->deselect();
|
||||
searchModel_->deselect();
|
||||
} else {
|
||||
// selected
|
||||
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
|
||||
if (convInfo.uid.isEmpty())
|
||||
return;
|
||||
|
||||
auto& accInfo = lrcInstance_->getAccountInfo(convInfo.accountId);
|
||||
accInfo.conversationModel->selectConversation(convInfo.uid);
|
||||
accInfo.conversationModel->clearUnreadInteractions(convInfo.uid);
|
||||
|
||||
try {
|
||||
// Set contact filter (for conversation tab selection)
|
||||
// WARNING: not swarm ready
|
||||
auto& contact = accInfo.contactModel->getContact(convInfo.participants.front());
|
||||
if (contact.profileInfo.type != profile::Type::INVALID
|
||||
&& contact.profileInfo.type != profile::Type::TEMPORARY)
|
||||
set_currentTypeFilter(contact.profileInfo.type);
|
||||
} catch (const std::out_of_range& e) {
|
||||
qWarning() << e.what();
|
||||
}
|
||||
|
||||
// reposition index in case of programmatic selection
|
||||
// currently, this may only occur for the conversation list
|
||||
// and not the search list
|
||||
convModel_->selectSourceRow(lrcInstance_->indexOf(convId));
|
||||
}
|
||||
});
|
||||
|
||||
connect(lrcInstance_, &LRCInstance::draftSaved, [this](const QString& convId) {
|
||||
auto row = lrcInstance_->indexOf(convId);
|
||||
const auto index = convSrcModel_->index(row, 0);
|
||||
Q_EMIT convSrcModel_->dataChanged(index, index);
|
||||
});
|
||||
|
||||
connect(lrcInstance_, &LRCInstance::contactBanned, [this](const QString& uri) {
|
||||
auto& convInfo = lrcInstance_->getConversationFromPeerUri(uri);
|
||||
if (convInfo.uid.isEmpty())
|
||||
return;
|
||||
auto row = lrcInstance_->indexOf(convInfo.uid);
|
||||
const auto index = convSrcModel_->index(row, 0);
|
||||
Q_EMIT convSrcModel_->dataChanged(index, index);
|
||||
});
|
||||
|
||||
updateConversationFilterData();
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
// notification responses
|
||||
connect(systemTray_,
|
||||
&SystemTray::openConversationActivated,
|
||||
[this](const QString& accountId, const QString& convUid) {
|
||||
Q_EMIT lrcInstance_->notificationClicked();
|
||||
selectConversation(accountId, convUid);
|
||||
Q_EMIT lrcInstance_->updateSmartList();
|
||||
Q_EMIT modelSorted(convUid);
|
||||
lrcInstance_->selectConversation(convUid, accountId);
|
||||
});
|
||||
connect(systemTray_,
|
||||
&SystemTray::acceptPendingActivated,
|
||||
|
@ -80,85 +133,71 @@ ConversationsAdapter::ConversationsAdapter(SystemTray* systemTray,
|
|||
void
|
||||
ConversationsAdapter::safeInit()
|
||||
{
|
||||
// TODO: remove these safeInits, they are possibly called
|
||||
// multiple times during qml component inits
|
||||
conversationSmartListModel_ = new SmartListModel(this,
|
||||
SmartListModel::Type::CONVERSATION,
|
||||
lrcInstance_);
|
||||
|
||||
Q_EMIT modelChanged(QVariant::fromValue(conversationSmartListModel_));
|
||||
|
||||
connect(&lrcInstance_->behaviorController(),
|
||||
&BehaviorController::showChatView,
|
||||
[this](const QString& accountId, const QString& convId) {
|
||||
Q_EMIT showConversation(accountId, convId);
|
||||
});
|
||||
|
||||
connect(&lrcInstance_->behaviorController(),
|
||||
&BehaviorController::newUnreadInteraction,
|
||||
this,
|
||||
&ConversationsAdapter::onNewUnreadInteraction);
|
||||
&ConversationsAdapter::onNewUnreadInteraction,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
connect(&lrcInstance_->behaviorController(),
|
||||
&BehaviorController::newReadInteraction,
|
||||
this,
|
||||
&ConversationsAdapter::onNewReadInteraction);
|
||||
&ConversationsAdapter::onNewReadInteraction,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
connect(&lrcInstance_->behaviorController(),
|
||||
&BehaviorController::newTrustRequest,
|
||||
this,
|
||||
&ConversationsAdapter::onNewTrustRequest);
|
||||
&ConversationsAdapter::onNewTrustRequest,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
connect(&lrcInstance_->behaviorController(),
|
||||
&BehaviorController::trustRequestTreated,
|
||||
this,
|
||||
&ConversationsAdapter::onTrustRequestTreated);
|
||||
&ConversationsAdapter::onTrustRequestTreated,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
connect(lrcInstance_,
|
||||
&LRCInstance::currentAccountChanged,
|
||||
this,
|
||||
&ConversationsAdapter::onCurrentAccountIdChanged);
|
||||
&ConversationsAdapter::onCurrentAccountIdChanged,
|
||||
Qt::UniqueConnection);
|
||||
|
||||
connectConversationModel();
|
||||
|
||||
setProperty("currentTypeFilter",
|
||||
QVariant::fromValue(lrcInstance_->getCurrentAccountInfo().profileInfo.type));
|
||||
set_currentTypeFilter(lrcInstance_->getCurrentAccountInfo().profileInfo.type);
|
||||
}
|
||||
|
||||
void
|
||||
ConversationsAdapter::backToWelcomePage()
|
||||
{
|
||||
deselectConversation();
|
||||
lrcInstance_->deselectConversation();
|
||||
Q_EMIT navigateToWelcomePageRequested();
|
||||
}
|
||||
|
||||
void
|
||||
ConversationsAdapter::selectConversation(const QString& accountId, const QString& convUid)
|
||||
{
|
||||
lrcInstance_->selectConversation(accountId, convUid);
|
||||
}
|
||||
|
||||
void
|
||||
ConversationsAdapter::deselectConversation()
|
||||
{
|
||||
if (lrcInstance_->get_selectedConvUid().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
|
||||
|
||||
if (currentConversationModel == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
lrcInstance_->set_selectedConvUid();
|
||||
}
|
||||
|
||||
void
|
||||
ConversationsAdapter::onCurrentAccountIdChanged()
|
||||
{
|
||||
lrcInstance_->deselectConversation();
|
||||
|
||||
convSrcModel_.reset(new ConversationListModel(lrcInstance_));
|
||||
convModel_->bindSourceModel(convSrcModel_.get());
|
||||
searchSrcModel_.reset(new SearchResultsListModel(lrcInstance_));
|
||||
searchModel_->bindSourceModel(searchSrcModel_.get());
|
||||
|
||||
connectConversationModel();
|
||||
|
||||
setProperty("currentTypeFilter",
|
||||
QVariant::fromValue(lrcInstance_->getCurrentAccountInfo().profileInfo.type));
|
||||
updateConversationFilterData();
|
||||
|
||||
set_currentTypeFilter(lrcInstance_->getCurrentAccountInfo().profileInfo.type);
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -189,11 +228,9 @@ ConversationsAdapter::onNewUnreadInteraction(const QString& accountId,
|
|||
auto onClicked = [this, accountId, convUid, uri = interaction.authorUri] {
|
||||
Q_EMIT lrcInstance_->notificationClicked();
|
||||
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convUid, accountId);
|
||||
if (!convInfo.uid.isEmpty()) {
|
||||
selectConversation(accountId, convInfo.uid);
|
||||
Q_EMIT lrcInstance_->updateSmartList();
|
||||
Q_EMIT modelSorted(convInfo.uid);
|
||||
}
|
||||
if (convInfo.uid.isEmpty())
|
||||
return;
|
||||
lrcInstance_->selectConversation(convInfo.uid, accountId);
|
||||
};
|
||||
systemTray_->showNotification(interaction.body, from, onClicked);
|
||||
#endif
|
||||
|
@ -209,6 +246,10 @@ ConversationsAdapter::onNewReadInteraction(const QString& accountId,
|
|||
// hide notification
|
||||
auto notifId = QString("%1;%2;%3").arg(accountId).arg(convUid).arg(interactionId);
|
||||
systemTray_->hideNotification(notifId);
|
||||
#else
|
||||
Q_UNUSED(accountId)
|
||||
Q_UNUSED(convUid)
|
||||
Q_UNUSED(interactionId)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -227,6 +268,9 @@ ConversationsAdapter::onNewTrustRequest(const QString& accountId, const QString&
|
|||
NotificationType::REQUEST,
|
||||
Utils::QImageToByteArray(contactPhoto));
|
||||
}
|
||||
#else
|
||||
Q_UNUSED(accountId)
|
||||
Q_UNUSED(peerUri)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -237,6 +281,9 @@ ConversationsAdapter::onTrustRequestTreated(const QString& accountId, const QStr
|
|||
// hide notification
|
||||
auto notifId = QString("%1;%2").arg(accountId).arg(peerUri);
|
||||
systemTray_->hideNotification(notifId);
|
||||
#else
|
||||
Q_UNUSED(accountId)
|
||||
Q_UNUSED(peerUri)
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -244,46 +291,30 @@ void
|
|||
ConversationsAdapter::onModelChanged()
|
||||
{
|
||||
conversationSmartListModel_->fillConversationsList();
|
||||
updateConversationsFilterWidget();
|
||||
|
||||
auto* convModel = lrcInstance_->getCurrentConversationModel();
|
||||
const auto& convInfo = lrcInstance_->getConversationFromConvUid(
|
||||
lrcInstance_->get_selectedConvUid());
|
||||
|
||||
if (convInfo.uid.isEmpty() || convInfo.participants.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const auto contactURI = convInfo.participants[0];
|
||||
if (contactURI.isEmpty()
|
||||
|| convModel->owner.contactModel->getContact(contactURI).profileInfo.type
|
||||
== lrc::api::profile::Type::TEMPORARY) {
|
||||
return;
|
||||
}
|
||||
Q_EMIT modelSorted(QVariant::fromValue(convInfo.uid));
|
||||
updateConversationFilterData();
|
||||
}
|
||||
|
||||
void
|
||||
ConversationsAdapter::onProfileUpdated(const QString& contactUri)
|
||||
{
|
||||
// TODO: this will need a dataChanged call to keep the avatar
|
||||
// updated. previously, 'reload-smartlist' was invoked here
|
||||
conversationSmartListModel_->updateContactAvatarUid(contactUri);
|
||||
Q_EMIT updateListViewRequested();
|
||||
}
|
||||
|
||||
void
|
||||
ConversationsAdapter::onConversationUpdated(const QString&)
|
||||
{
|
||||
updateConversationsFilterWidget();
|
||||
Q_EMIT updateListViewRequested();
|
||||
updateConversationFilterData();
|
||||
}
|
||||
|
||||
void
|
||||
ConversationsAdapter::onFilterChanged()
|
||||
{
|
||||
conversationSmartListModel_->fillConversationsList();
|
||||
updateConversationsFilterWidget();
|
||||
updateConversationFilterData();
|
||||
if (!lrcInstance_->get_selectedConvUid().isEmpty())
|
||||
Q_EMIT indexRepositionRequested();
|
||||
Q_EMIT updateListViewRequested();
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -304,10 +335,9 @@ ConversationsAdapter::onConversationCleared(const QString& convUid)
|
|||
{
|
||||
// If currently selected, switch to welcome screen (deselecting
|
||||
// current smartlist item).
|
||||
if (convUid != lrcInstance_->get_selectedConvUid()) {
|
||||
return;
|
||||
if (convUid == lrcInstance_->get_selectedConvUid()) {
|
||||
lrcInstance_->deselectConversation();
|
||||
}
|
||||
backToWelcomePage();
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -319,26 +349,94 @@ ConversationsAdapter::onSearchStatusChanged(const QString& status)
|
|||
void
|
||||
ConversationsAdapter::onSearchResultUpdated()
|
||||
{
|
||||
// currently for contact pickers
|
||||
conversationSmartListModel_->fillConversationsList();
|
||||
Q_EMIT updateListViewRequested();
|
||||
|
||||
// smartlist search results
|
||||
searchSrcModel_->onSearchResultsUpdated();
|
||||
}
|
||||
|
||||
void
|
||||
ConversationsAdapter::updateConversationsFilterWidget()
|
||||
ConversationsAdapter::updateConversationFilterData()
|
||||
{
|
||||
// Update status of "Conversations" and "Invitations".
|
||||
auto invites = lrcInstance_->getCurrentAccountInfo().contactModel->pendingRequestCount();
|
||||
if (invites == 0 && currentTypeFilter_ == lrc::api::profile::Type::PENDING) {
|
||||
setProperty("currentTypeFilter", QVariant::fromValue(lrc::api::profile::Type::RING));
|
||||
// TODO: this may be further spliced to respond separately to
|
||||
// incoming messages and invites
|
||||
// total unread message and pending invite counts, and tab selection
|
||||
auto& accountInfo = lrcInstance_->getCurrentAccountInfo();
|
||||
int totalUnreadMessages {0};
|
||||
if (accountInfo.profileInfo.type != profile::Type::SIP) {
|
||||
auto& convModel = accountInfo.conversationModel;
|
||||
auto conversations = convModel->getFilteredConversations(profile::Type::RING, false);
|
||||
conversations.for_each([&totalUnreadMessages](const conversation::Info& conversation) {
|
||||
totalUnreadMessages += conversation.unreadMessages;
|
||||
});
|
||||
}
|
||||
set_totalUnreadMessageCount(totalUnreadMessages);
|
||||
set_pendingRequestCount(accountInfo.contactModel->pendingRequestCount());
|
||||
if (pendingRequestCount_ == 0 && currentTypeFilter_ == profile::Type::PENDING) {
|
||||
set_currentTypeFilter(profile::Type::RING);
|
||||
}
|
||||
showConversationTabs(invites);
|
||||
}
|
||||
|
||||
void
|
||||
ConversationsAdapter::refill()
|
||||
ConversationsAdapter::setFilter(const QString& filterString)
|
||||
{
|
||||
if (conversationSmartListModel_)
|
||||
conversationSmartListModel_->fillConversationsList();
|
||||
convModel_->setFilter(filterString);
|
||||
searchSrcModel_->setFilter(filterString);
|
||||
}
|
||||
|
||||
void
|
||||
ConversationsAdapter::setTypeFilter(const profile::Type& typeFilter)
|
||||
{
|
||||
convModel_->setTypeFilter(typeFilter);
|
||||
}
|
||||
|
||||
QVariantMap
|
||||
ConversationsAdapter::getConvInfoMap(const QString& convId)
|
||||
{
|
||||
const auto& convInfo = lrcInstance_->getConversationFromConvUid(convId);
|
||||
if (convInfo.participants.empty())
|
||||
return {};
|
||||
auto peerUri = convInfo.participants[0];
|
||||
ContactModel* contactModel {nullptr};
|
||||
contact::Info contact {};
|
||||
try {
|
||||
const auto& accountInfo = lrcInstance_->getAccountInfo(convInfo.accountId);
|
||||
contactModel = accountInfo.contactModel.get();
|
||||
contact = contactModel->getContact(peerUri);
|
||||
} catch (...) {
|
||||
return {};
|
||||
}
|
||||
bool isAudioOnly {false};
|
||||
if (!convInfo.uid.isEmpty()) {
|
||||
auto* call = lrcInstance_->getCallInfoForConversation(convInfo);
|
||||
if (call) {
|
||||
isAudioOnly = call->isAudioOnly;
|
||||
}
|
||||
}
|
||||
bool callStackViewShouldShow {false};
|
||||
call::Status callState {};
|
||||
if (!convInfo.callId.isEmpty()) {
|
||||
auto* callModel = lrcInstance_->getCurrentCallModel();
|
||||
const auto& call = callModel->getCall(convInfo.callId);
|
||||
callStackViewShouldShow = callModel->hasCall(convInfo.callId)
|
||||
&& ((!call.isOutgoing
|
||||
&& (call.status == call::Status::IN_PROGRESS
|
||||
|| call.status == call::Status::PAUSED
|
||||
|| call.status == call::Status::INCOMING_RINGING))
|
||||
|| (call.isOutgoing && call.status != call::Status::ENDED));
|
||||
callState = call.status;
|
||||
}
|
||||
// WARNING: not swarm ready
|
||||
// titles should come from conversation, not contact model
|
||||
return {{"convId", convId},
|
||||
{"bestId", contactModel->bestIdForContact(peerUri)},
|
||||
{"bestName", contactModel->bestNameForContact(peerUri)},
|
||||
{"uri", peerUri},
|
||||
{"contactType", static_cast<int>(contact.profileInfo.type)},
|
||||
{"isAudioOnly", isAudioOnly},
|
||||
{"callState", static_cast<int>(callState)},
|
||||
{"callStackViewShouldShow", callStackViewShouldShow}};
|
||||
}
|
||||
|
||||
bool
|
||||
|
@ -402,7 +500,7 @@ ConversationsAdapter::connectConversationModel(bool updateFilter)
|
|||
Qt::UniqueConnection);
|
||||
|
||||
if (updateFilter) {
|
||||
currentTypeFilter_ = lrc::api::profile::Type::INVALID;
|
||||
currentTypeFilter_ = profile::Type::INVALID;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -421,8 +519,8 @@ ConversationsAdapter::updateConversationForNewContact(const QString& convUid)
|
|||
const auto contact = convModel->owner.contactModel->getContact(convInfo.participants[0]);
|
||||
if (!contact.profileInfo.uri.isEmpty()
|
||||
&& contact.profileInfo.uri == lrcInstance_->get_selectedConvUid()) {
|
||||
lrcInstance_->set_selectedConvUid(convUid);
|
||||
convModel->selectConversation(convUid);
|
||||
lrcInstance_->selectConversation(convUid, convInfo.accountId);
|
||||
convModel_->selectSourceRow(lrcInstance_->indexOf(convUid));
|
||||
}
|
||||
} catch (...) {
|
||||
return;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*!
|
||||
/*
|
||||
* Copyright (C) 2020 by Savoir-faire Linux
|
||||
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -21,6 +22,8 @@
|
|||
#include "lrcinstance.h"
|
||||
#include "qmladapterbase.h"
|
||||
#include "smartlistmodel.h"
|
||||
#include "conversationlistmodel.h"
|
||||
#include "searchresultslistmodel.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
|
@ -30,9 +33,10 @@ class SystemTray;
|
|||
class ConversationsAdapter final : public QmlAdapterBase
|
||||
{
|
||||
Q_OBJECT
|
||||
QML_PROPERTY(lrc::api::profile::Type, currentTypeFilter)
|
||||
QML_PROPERTY(int, totalUnreadMessageCount)
|
||||
QML_PROPERTY(int, pendingRequestCount)
|
||||
|
||||
Q_PROPERTY(lrc::api::profile::Type currentTypeFilter MEMBER currentTypeFilter_ NOTIFY
|
||||
currentTypeFilterChanged)
|
||||
public:
|
||||
explicit ConversationsAdapter(SystemTray* systemTray,
|
||||
LRCInstance* instance,
|
||||
|
@ -44,25 +48,22 @@ protected:
|
|||
|
||||
public:
|
||||
Q_INVOKABLE bool connectConversationModel(bool updateFilter = true);
|
||||
Q_INVOKABLE void selectConversation(const QString& accountId, const QString& uid);
|
||||
Q_INVOKABLE void deselectConversation();
|
||||
Q_INVOKABLE void refill();
|
||||
Q_INVOKABLE void updateConversationsFilterWidget();
|
||||
Q_INVOKABLE void setFilter(const QString& filterString);
|
||||
Q_INVOKABLE void setTypeFilter(const profile::Type& typeFilter);
|
||||
Q_INVOKABLE QVariantMap getConvInfoMap(const QString& convId);
|
||||
|
||||
Q_SIGNALS:
|
||||
void showConversation(const QString& accountId, const QString& convUid);
|
||||
void showConversationTabs(bool visible);
|
||||
void showSearchStatus(const QString& status);
|
||||
|
||||
void modelChanged(const QVariant& model);
|
||||
void modelSorted(const QVariant& uid);
|
||||
void updateListViewRequested();
|
||||
void navigateToWelcomePageRequested();
|
||||
void currentTypeFilterChanged();
|
||||
void indexRepositionRequested();
|
||||
|
||||
private Q_SLOTS:
|
||||
void onCurrentAccountIdChanged();
|
||||
|
||||
// cross-account slots
|
||||
void onNewUnreadInteraction(const QString& accountId,
|
||||
const QString& convUid,
|
||||
uint64_t interactionId,
|
||||
|
@ -73,6 +74,7 @@ private Q_SLOTS:
|
|||
void onNewTrustRequest(const QString& accountId, const QString& peerUri);
|
||||
void onTrustRequestTreated(const QString& accountId, const QString& peerUri);
|
||||
|
||||
// per-account slots
|
||||
void onModelChanged();
|
||||
void onProfileUpdated(const QString&);
|
||||
void onConversationUpdated(const QString&);
|
||||
|
@ -83,13 +85,18 @@ private Q_SLOTS:
|
|||
void onSearchStatusChanged(const QString&);
|
||||
void onSearchResultUpdated();
|
||||
|
||||
void updateConversationFilterData();
|
||||
|
||||
private:
|
||||
void backToWelcomePage();
|
||||
void updateConversationForNewContact(const QString& convUid);
|
||||
|
||||
SmartListModel* conversationSmartListModel_;
|
||||
|
||||
lrc::api::profile::Type currentTypeFilter_ {};
|
||||
|
||||
SystemTray* systemTray_;
|
||||
|
||||
QScopedPointer<ConversationListModel> convSrcModel_;
|
||||
QScopedPointer<ConversationListProxyModel> convModel_;
|
||||
QScopedPointer<SearchResultsListModel> searchSrcModel_;
|
||||
QScopedPointer<SelectableListProxyModel> searchModel_;
|
||||
};
|
||||
|
|
|
@ -219,6 +219,12 @@ LRCInstance::getCurrentCallModel()
|
|||
return getCurrentAccountInfo().callModel.get();
|
||||
}
|
||||
|
||||
ContactModel*
|
||||
LRCInstance::getCurrentContactModel()
|
||||
{
|
||||
return getCurrentAccountInfo().contactModel.get();
|
||||
}
|
||||
|
||||
const QString&
|
||||
LRCInstance::getCurrAccId()
|
||||
{
|
||||
|
@ -301,6 +307,18 @@ LRCInstance::getCurrAccConfig()
|
|||
return getCurrentAccountInfo().confProperties;
|
||||
}
|
||||
|
||||
int
|
||||
LRCInstance::indexOf(const QString& convId)
|
||||
{
|
||||
auto& convs = getCurrentConversationModel()->getConversations();
|
||||
auto it = std::find_if(convs.begin(),
|
||||
convs.end(),
|
||||
[convId](const lrc::api::conversation::Info& conv) {
|
||||
return conv.uid == convId;
|
||||
});
|
||||
return it != convs.end() ? std::distance(convs.begin(), it) : -1;
|
||||
}
|
||||
|
||||
void
|
||||
LRCInstance::subscribeToDebugReceived()
|
||||
{
|
||||
|
@ -353,6 +371,8 @@ LRCInstance::setContentDraft(const QString& convUid,
|
|||
{
|
||||
auto draftKey = accountId + "_" + convUid;
|
||||
contentDrafts_[draftKey] = content;
|
||||
// this signal is only needed to update the current smartlist
|
||||
Q_EMIT draftSaved(convUid);
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -374,41 +394,24 @@ LRCInstance::poplastConference(const QString& confId)
|
|||
}
|
||||
|
||||
void
|
||||
LRCInstance::selectConversation(const QString& accountId, const QString& convUid)
|
||||
LRCInstance::selectConversation(const QString& convId, const QString& accountId)
|
||||
{
|
||||
const auto& convInfo = getConversationFromConvUid(convUid, accountId);
|
||||
|
||||
if (get_selectedConvUid() != convInfo.uid || convInfo.participants.size() > 0) {
|
||||
// If the account is not currently selected, do that first, then
|
||||
// proceed to select the conversation.
|
||||
auto selectConversation = [this, accountId, convUid = convInfo.uid] {
|
||||
const auto& convInfo = getConversationFromConvUid(convUid, accountId);
|
||||
if (convInfo.uid.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
auto& accInfo = getAccountInfo(convInfo.accountId);
|
||||
set_selectedConvUid(convInfo.uid);
|
||||
accInfo.conversationModel->clearUnreadInteractions(convInfo.uid);
|
||||
|
||||
try {
|
||||
// Set contact filter (for conversation tab selection)
|
||||
auto& contact = accInfo.contactModel->getContact(convInfo.participants.front());
|
||||
setProperty("currentTypeFilter", QVariant::fromValue(contact.profileInfo.type));
|
||||
} catch (const std::out_of_range& e) {
|
||||
qDebug() << e.what();
|
||||
}
|
||||
};
|
||||
if (convInfo.accountId != getCurrAccId()) {
|
||||
Utils::oneShotConnect(this, &LRCInstance::currentAccountChanged, [selectConversation] {
|
||||
selectConversation();
|
||||
});
|
||||
set_selectedConvUid();
|
||||
setSelectedAccountId(convInfo.accountId);
|
||||
} else {
|
||||
selectConversation();
|
||||
}
|
||||
// if the account is not currently selected, do that first, then
|
||||
// proceed to select the conversation
|
||||
if (!accountId.isEmpty() && accountId != getCurrAccId()) {
|
||||
Utils::oneShotConnect(this, &LRCInstance::currentAccountChanged, [this, convId] {
|
||||
set_selectedConvUid(convId);
|
||||
});
|
||||
setSelectedAccountId(accountId);
|
||||
return;
|
||||
}
|
||||
Q_EMIT conversationSelected();
|
||||
set_selectedConvUid(convId);
|
||||
}
|
||||
|
||||
void
|
||||
LRCInstance::deselectConversation()
|
||||
{
|
||||
set_selectedConvUid();
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -72,6 +72,7 @@ public:
|
|||
NewAccountModel& accountModel();
|
||||
ConversationModel* getCurrentConversationModel();
|
||||
NewCallModel* getCurrentCallModel();
|
||||
ContactModel* getCurrentContactModel();
|
||||
AVModel& avModel();
|
||||
PluginModel& pluginModel();
|
||||
BehaviorController& behaviorController();
|
||||
|
@ -95,15 +96,18 @@ public:
|
|||
const conversation::Info& getConversationFromCallId(const QString& callId,
|
||||
const QString& accountId = {});
|
||||
|
||||
Q_INVOKABLE void selectConversation(const QString& convId, const QString& accountId = {});
|
||||
Q_INVOKABLE void deselectConversation();
|
||||
|
||||
const QString& getCurrAccId();
|
||||
void setSelectedAccountId(const QString& accountId = {});
|
||||
void selectConversation(const QString& accountId, const QString& convUid);
|
||||
int getCurrentAccountIndex();
|
||||
void setAvatarForAccount(const QPixmap& avatarPixmap, const QString& accountID);
|
||||
void setCurrAccAvatar(const QPixmap& avatarPixmap);
|
||||
void setCurrAccAvatar(const QString& avatar);
|
||||
void setCurrAccDisplayName(const QString& displayName);
|
||||
const account::ConfProperties_t& getCurrAccConfig();
|
||||
int indexOf(const QString& convId);
|
||||
|
||||
void startAudioMeter(bool async);
|
||||
void stopAudioMeter(bool async);
|
||||
|
@ -121,15 +125,16 @@ Q_SIGNALS:
|
|||
void currentAccountChanged();
|
||||
void restoreAppRequested();
|
||||
void notificationClicked();
|
||||
void updateSmartList();
|
||||
void quitEngineRequested();
|
||||
void conversationSelected();
|
||||
void conversationUpdated(const QString& convId, const QString& accountId);
|
||||
void draftSaved(const QString& convId);
|
||||
void contactBanned(const QString& uri);
|
||||
|
||||
private:
|
||||
std::unique_ptr<Lrc> lrc_;
|
||||
std::unique_ptr<RenderManager> renderer_;
|
||||
std::unique_ptr<UpdateManager> updateManager_;
|
||||
QString selectedAccountId_ {""};
|
||||
QString selectedAccountId_ {};
|
||||
MapStringString contentDrafts_;
|
||||
MapStringString lastConferences_;
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
class ConnectivityMonitor;
|
||||
class AppSettingsManager;
|
||||
class SystemTray;
|
||||
class CallAdapter;
|
||||
|
||||
// Provides information about the screen the app is displayed on
|
||||
class ScreenInfo : public QObject
|
||||
|
@ -98,4 +99,6 @@ private:
|
|||
SystemTray* systemTray_;
|
||||
|
||||
ScreenInfo screenInfo_;
|
||||
|
||||
CallAdapter* callAdapter_;
|
||||
};
|
||||
|
|
|
@ -59,8 +59,6 @@ Rectangle {
|
|||
|
||||
property string currentAccountId: AccountAdapter.currentAccountId
|
||||
onCurrentAccountIdChanged: {
|
||||
var index = UtilsAdapter.getCurrAccList().indexOf(currentAccountId)
|
||||
mainViewSidePanel.refreshAccountComboBox(index)
|
||||
if (inSettingsView) {
|
||||
settingsView.accountListChanged()
|
||||
settingsView.setSelected(settingsView.selectedMenu, true)
|
||||
|
@ -80,7 +78,7 @@ Rectangle {
|
|||
function showWelcomeView() {
|
||||
currentConvUID = ""
|
||||
callStackView.needToCloseInCallConversationAndPotentialWindow()
|
||||
mainViewSidePanel.deselectConversationSmartList()
|
||||
LRCInstance.deselectConversation()
|
||||
if (isPageInStack("callStackViewObject", sidePanelViewStack) ||
|
||||
isPageInStack("communicationPageMessageWebView", sidePanelViewStack) ||
|
||||
isPageInStack("communicationPageMessageWebView", mainViewStack) ||
|
||||
|
@ -140,8 +138,7 @@ Rectangle {
|
|||
if (checkCurrentCall && currentAccountIsCalling()) {
|
||||
var callConv = UtilsAdapter.getCallConvForAccount(
|
||||
AccountAdapter.currentAccountId)
|
||||
ConversationsAdapter.selectConversation(
|
||||
AccountAdapter.currentAccountId, callConv)
|
||||
LRCInstance.selectConversation(callConv)
|
||||
CallAdapter.updateCall(callConv, currentAccountId)
|
||||
} else {
|
||||
showWelcomeView()
|
||||
|
@ -171,56 +168,50 @@ Rectangle {
|
|||
}
|
||||
}
|
||||
|
||||
// ConversationSmartListViewItemDelegate provides UI information
|
||||
function setMainView(currentUserDisplayName, currentUserAlias, currentUID,
|
||||
callStackViewShouldShow, isAudioOnly, callState) {
|
||||
function setMainView(convId) {
|
||||
if (!(communicationPageMessageWebView.jsLoaded)) {
|
||||
communicationPageMessageWebView.jsLoadedChanged.connect(
|
||||
function(currentUserDisplayName, currentUserAlias, currentUID,
|
||||
callStackViewShouldShow, isAudioOnly, callState) {
|
||||
return function() {
|
||||
setMainView(currentUserDisplayName, currentUserAlias, currentUID,
|
||||
callStackViewShouldShow, isAudioOnly, callState)
|
||||
}
|
||||
}(currentUserDisplayName, currentUserAlias, currentUID,
|
||||
callStackViewShouldShow, isAudioOnly, callState))
|
||||
function(convId) {
|
||||
return function() { setMainView(convId) }
|
||||
}(convId))
|
||||
return
|
||||
}
|
||||
|
||||
if (callStackViewShouldShow) {
|
||||
var item = ConversationsAdapter.getConvInfoMap(convId)
|
||||
if (item.convId === undefined)
|
||||
return
|
||||
communicationPageMessageWebView.headerUserAliasLabelText = item.bestName
|
||||
communicationPageMessageWebView.headerUserUserNameLabelText = item.bestId
|
||||
if (item.callStackViewShouldShow) {
|
||||
if (inSettingsView) {
|
||||
toggleSettingsView()
|
||||
}
|
||||
MessagesAdapter.setupChatView(currentUID)
|
||||
communicationPageMessageWebView.headerUserAliasLabelText = currentUserAlias
|
||||
communicationPageMessageWebView.headerUserUserNameLabelText = currentUserDisplayName
|
||||
MessagesAdapter.setupChatView(convId)
|
||||
callStackView.setLinkedWebview(communicationPageMessageWebView)
|
||||
callStackView.responsibleAccountId = AccountAdapter.currentAccountId
|
||||
callStackView.responsibleConvUid = currentUID
|
||||
currentConvUID = currentUID
|
||||
callStackView.responsibleConvUid = convId
|
||||
currentConvUID = convId
|
||||
|
||||
if (callState === Call.Status.IN_PROGRESS || callState === Call.Status.PAUSED) {
|
||||
CallAdapter.updateCall(currentUID, AccountAdapter.currentAccountId)
|
||||
if (isAudioOnly)
|
||||
if (item.callState === Call.Status.IN_PROGRESS ||
|
||||
item.callState === Call.Status.PAUSED) {
|
||||
CallAdapter.updateCall(convId, AccountAdapter.currentAccountId)
|
||||
if (item.isAudioOnly)
|
||||
callStackView.showAudioCallPage()
|
||||
else
|
||||
callStackView.showVideoCallPage()
|
||||
} else if (callState === Call.Status.INCOMING_RINGING) {
|
||||
} else if (item.callState === Call.Status.INCOMING_RINGING) {
|
||||
callStackView.showIncomingCallPage()
|
||||
} else {
|
||||
callStackView.showOutgoingCallPage(callState)
|
||||
callStackView.showOutgoingCallPage(item.callState)
|
||||
}
|
||||
pushCallStackView()
|
||||
|
||||
} else if (!inSettingsView) {
|
||||
if (currentConvUID !== currentUID) {
|
||||
if (currentConvUID !== convId) {
|
||||
callStackView.needToCloseInCallConversationAndPotentialWindow()
|
||||
MessagesAdapter.setupChatView(currentUID)
|
||||
communicationPageMessageWebView.headerUserAliasLabelText = currentUserAlias
|
||||
communicationPageMessageWebView.headerUserUserNameLabelText = currentUserDisplayName
|
||||
MessagesAdapter.setupChatView(convId)
|
||||
pushCommunicationMessageWebView()
|
||||
communicationPageMessageWebView.focusMessageWebView()
|
||||
currentConvUID = currentUID
|
||||
currentConvUID = convId
|
||||
} else if (isPageInStack("callStackViewObject", sidePanelViewStack)
|
||||
|| isPageInStack("callStackViewObject", mainViewStack)) {
|
||||
callStackView.needToCloseInCallConversationAndPotentialWindow()
|
||||
|
@ -233,11 +224,16 @@ Rectangle {
|
|||
color: JamiTheme.backgroundColor
|
||||
|
||||
Connections {
|
||||
target: CallAdapter
|
||||
target: LRCInstance
|
||||
|
||||
// selectConversation causes UI update
|
||||
function onCallSetupMainViewRequired(accountId, convUid) {
|
||||
ConversationsAdapter.selectConversation(accountId, convUid)
|
||||
function onSelectedConvUidChanged() {
|
||||
mainView.setMainView(LRCInstance.selectedConvUid)
|
||||
}
|
||||
|
||||
function onConversationUpdated(convUid, accountId) {
|
||||
if (convUid === LRCInstance.selectedConvUid &&
|
||||
accountId === currentAccountId)
|
||||
mainView.setMainView(convUid)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -291,12 +287,6 @@ Rectangle {
|
|||
Connections {
|
||||
target: AccountAdapter
|
||||
|
||||
function onUpdateConversationForAddedContact() {
|
||||
MessagesAdapter.updateConversationForAddedContact()
|
||||
mainViewSidePanel.clearContactSearchBar()
|
||||
mainViewSidePanel.forceReselectConversationSmartListCurrentIndex()
|
||||
}
|
||||
|
||||
function onAccountStatusChanged(accountId) {
|
||||
accountComboBox.resetAccountListModel(accountId)
|
||||
}
|
||||
|
@ -438,17 +428,12 @@ Rectangle {
|
|||
Connections {
|
||||
target: MessagesAdapter
|
||||
|
||||
function onNeedToUpdateSmartList() {
|
||||
mainViewSidePanel.forceUpdateConversationSmartListView()
|
||||
}
|
||||
|
||||
function onNavigateToWelcomePageRequested() {
|
||||
backToMainView()
|
||||
}
|
||||
|
||||
function onInvitationAccepted() {
|
||||
mainViewSidePanel.selectTab(SidePanelTabBar.Conversations)
|
||||
showWelcomeView()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,7 +34,20 @@ Label {
|
|||
signal settingBtnClicked
|
||||
property alias popup: comboBoxPopup
|
||||
|
||||
// Reset accountListModel.
|
||||
// TODO: remove these refresh hacks use QAbstractItemModels correctly
|
||||
Connections {
|
||||
target: AccountAdapter
|
||||
|
||||
function onCurrentAccountIdChanged() {
|
||||
root.update()
|
||||
resetAccountListModel(AccountAdapter.currentAccountId)
|
||||
}
|
||||
|
||||
function onAccountStatusChanged(accountId) {
|
||||
resetAccountListModel(accountId)
|
||||
}
|
||||
}
|
||||
|
||||
function resetAccountListModel(accountId) {
|
||||
accountListModel.updateAvatarUid(accountId)
|
||||
accountListModel.reset()
|
||||
|
|
|
@ -112,11 +112,11 @@ Popup {
|
|||
layer {
|
||||
enabled: true
|
||||
effect: DropShadow {
|
||||
color: JamiTheme.shadowColor
|
||||
verticalOffset: 2
|
||||
horizontalOffset: 2
|
||||
horizontalOffset: 3.0
|
||||
verticalOffset: 3.0
|
||||
radius: 16.0
|
||||
samples: 16
|
||||
radius: 10
|
||||
color: JamiTheme.shadowColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
79
src/mainview/components/BadgeNotifier.qml
Normal file
79
src/mainview/components/BadgeNotifier.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -399,7 +399,7 @@ Rectangle {
|
|||
onAddToConferenceButtonClicked: {
|
||||
// Create contact picker - conference.
|
||||
ContactPickerCreation.createContactPickerObjects(
|
||||
ContactPicker.ContactPickerType.JAMICONFERENCE,
|
||||
ContactList.CONFERENCE,
|
||||
callOverlayRect)
|
||||
ContactPickerCreation.openContactPicker()
|
||||
}
|
||||
|
@ -517,7 +517,7 @@ Rectangle {
|
|||
onTransferCallButtonClicked: {
|
||||
// Create contact picker - sip transfer.
|
||||
ContactPickerCreation.createContactPickerObjects(
|
||||
ContactPicker.ContactPickerType.SIPTRANSFER,
|
||||
ContactList.TRANSFER,
|
||||
callOverlayRect)
|
||||
ContactPickerCreation.openContactPicker()
|
||||
}
|
||||
|
|
|
@ -30,15 +30,7 @@ import "../../commoncomponents"
|
|||
Popup {
|
||||
id: contactPickerPopup
|
||||
|
||||
property int type: ContactPicker.ContactPickerType.JAMICONFERENCE
|
||||
|
||||
|
||||
// Important to keep it one, since enum in c++ starts at one for conferences.
|
||||
enum ContactPickerType {
|
||||
CONVERSATION = 0,
|
||||
JAMICONFERENCE,
|
||||
SIPTRANSFER
|
||||
}
|
||||
property int type: ContactList.CONFERENCE
|
||||
|
||||
contentWidth: 250
|
||||
contentHeight: contactPickerPopupRectColumnLayout.height + 50
|
||||
|
@ -89,9 +81,9 @@ Popup {
|
|||
|
||||
text: {
|
||||
switch(type) {
|
||||
case ContactPicker.ContactPickerType.JAMICONFERENCE:
|
||||
case ContactList.CONFERENCE:
|
||||
return qsTr("Add to conference")
|
||||
case ContactPicker.ContactPickerType.SIPTRANSFER:
|
||||
case ContactList.TRANSFER:
|
||||
return qsTr("Transfer this call")
|
||||
default:
|
||||
return qsTr("Add default moderator")
|
||||
|
|
|
@ -66,7 +66,7 @@ ItemDelegate {
|
|||
font: contactPickerContactName.font
|
||||
elide: Text.ElideMiddle
|
||||
elideWidth: contactPickerContactInfoRect.width
|
||||
text: DisplayName
|
||||
text: BestName
|
||||
}
|
||||
|
||||
color: JamiTheme.textColor
|
||||
|
@ -88,7 +88,7 @@ ItemDelegate {
|
|||
font: contactPickerContactId.font
|
||||
elide: Text.ElideMiddle
|
||||
elideWidth: contactPickerContactInfoRect.width
|
||||
text: DisplayID == DisplayName ? "" : DisplayID
|
||||
text: BestId == BestName ? "" : BestId
|
||||
}
|
||||
|
||||
text: textMetricsContactPickerContactId.elidedText
|
||||
|
|
|
@ -32,6 +32,8 @@ Rectangle {
|
|||
signal contactSearchBarTextChanged(string text)
|
||||
signal returnPressedWhileSearching
|
||||
|
||||
property alias textContent: contactSearchBar.text
|
||||
|
||||
function clearText() {
|
||||
contactSearchBar.clear()
|
||||
fakeFocus.forceActiveFocus()
|
||||
|
@ -108,10 +110,11 @@ Rectangle {
|
|||
anchors.right: root.right
|
||||
anchors.rightMargin: 10
|
||||
|
||||
preferredSize: 20
|
||||
preferredSize: 21
|
||||
radius: JamiTheme.primaryRadius
|
||||
|
||||
visible: contactSearchBar.text.length
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
normalColor: root.color
|
||||
imageColor: JamiTheme.primaryForegroundColor
|
||||
|
@ -120,6 +123,10 @@ Rectangle {
|
|||
toolTipText: JamiStrings.clearText
|
||||
|
||||
onClicked: contactSearchBar.clear()
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 500; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
|
|
222
src/mainview/components/ConversationListView.qml
Normal file
222
src/mainview/components/ConversationListView.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -34,6 +34,8 @@ Item {
|
|||
property string responsibleConvUid: ""
|
||||
property int contactType: Profile.Type.INVALID
|
||||
|
||||
function isOpen() { return ContextMenuGenerator.getMenu().visible }
|
||||
|
||||
function openMenu() {
|
||||
ContextMenuGenerator.initMenu()
|
||||
var hasCall = UtilsAdapter.getCallId(responsibleAccountId, responsibleConvUid) !== ""
|
||||
|
@ -41,18 +43,14 @@ Item {
|
|||
ContextMenuGenerator.addMenuItem(qsTr("Start video call"),
|
||||
"qrc:/images/icons/videocam-24px.svg",
|
||||
function (){
|
||||
ConversationsAdapter.selectConversation(
|
||||
responsibleAccountId,
|
||||
responsibleConvUid, false)
|
||||
LRCInstance.selectConversation(responsibleConvUid, responsibleAccountId)
|
||||
CallAdapter.placeCall()
|
||||
communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
|
||||
})
|
||||
ContextMenuGenerator.addMenuItem(qsTr("Start audio call"),
|
||||
"qrc:/images/icons/place_audiocall-24px.svg",
|
||||
function (){
|
||||
ConversationsAdapter.selectConversation(
|
||||
responsibleAccountId,
|
||||
responsibleConvUid, false)
|
||||
LRCInstance.selectConversation(responsibleConvUid, responsibleAccountId)
|
||||
CallAdapter.placeAudioOnlyCall()
|
||||
communicationPageMessageWebView.setSendContactRequestButtonVisible(false)
|
||||
})
|
||||
|
@ -116,7 +114,7 @@ Item {
|
|||
})
|
||||
}
|
||||
ContextMenuGenerator.addMenuSeparator()
|
||||
ContextMenuGenerator.addMenuItem(qsTr("Profile"),
|
||||
ContextMenuGenerator.addMenuItem(qsTr("Contact details"),
|
||||
"qrc:/images/icons/person-24px.svg",
|
||||
function (){
|
||||
userProfile.open()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,10 +20,7 @@
|
|||
import QtQuick 2.14
|
||||
import QtQuick.Controls 2.14
|
||||
import QtQuick.Layouts 1.14
|
||||
import QtGraphicalEffects 1.14
|
||||
|
||||
import net.jami.Models 1.0
|
||||
import net.jami.Adapters 1.0
|
||||
import net.jami.Constants 1.0
|
||||
|
||||
import "../../commoncomponents"
|
||||
|
@ -34,7 +31,7 @@ TabButton {
|
|||
property var tabBar: undefined
|
||||
property alias labelText: label.text
|
||||
property alias acceleratorSequence: accelerator.sequence
|
||||
property int badgeCount
|
||||
property alias badgeCount: badge.count
|
||||
signal selected
|
||||
|
||||
hoverEnabled: true
|
||||
|
@ -57,32 +54,17 @@ TabButton {
|
|||
id: label
|
||||
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
Layout.bottomMargin: 1
|
||||
|
||||
font.pointSize: JamiTheme.filterItemFontSize
|
||||
color: Qt.lighter(JamiTheme.textColor,
|
||||
root.down == true ? 1.0 : 1.5)
|
||||
color: JamiTheme.textColor
|
||||
opacity: root.down ? 1.0 : 0.5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: badgeRect
|
||||
|
||||
readonly property real size: 20
|
||||
|
||||
BadgeNotifier {
|
||||
id: badge
|
||||
size: 20
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
|
||||
|
||||
width: size
|
||||
height: size
|
||||
radius: JamiTheme.primaryRadius
|
||||
color: JamiTheme.filterBadgeColor
|
||||
|
||||
visible: badgeCount > 0
|
||||
|
||||
Text {
|
||||
anchors.centerIn: badgeRect
|
||||
text: badgeCount > 9 ? "…" : badgeCount
|
||||
color: JamiTheme.filterBadgeTextColor
|
||||
font.pointSize: JamiTheme.filterBadgeFontSize
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,9 +73,7 @@ TabButton {
|
|||
width: rect.width
|
||||
anchors.bottom: rect.bottom
|
||||
height: 2
|
||||
color: root.down === true ?
|
||||
JamiTheme.textColor :
|
||||
"transparent"
|
||||
color: root.down ? JamiTheme.textColor : "transparent"
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
|
|
|
@ -108,6 +108,14 @@ Rectangle {
|
|||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: AccountAdapter
|
||||
|
||||
function onSelectedContactAdded(convId) {
|
||||
MessagesAdapter.updateConversationForAddedContact()
|
||||
}
|
||||
}
|
||||
|
||||
JamiFileDialog {
|
||||
id: jamiFileDialog
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
* Copyright (C) 2020 by Savoir-faire Linux
|
||||
* Copyright (C) 2020-2021 by Savoir-faire Linux
|
||||
* Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
|
@ -32,60 +33,29 @@ Rectangle {
|
|||
|
||||
color: JamiTheme.backgroundColor
|
||||
|
||||
property bool tabBarVisible: true
|
||||
property int pendingRequestCount: 0
|
||||
property int totalUnreadMessagesCount: 0
|
||||
anchors.fill: parent
|
||||
|
||||
// Hack -> force redraw.
|
||||
function forceReselectConversationSmartListCurrentIndex() {
|
||||
var index = conversationSmartListView.currentIndex
|
||||
conversationSmartListView.currentIndex = -1
|
||||
conversationSmartListView.currentIndex = index
|
||||
}
|
||||
Connections {
|
||||
target: AccountAdapter
|
||||
|
||||
function onCurrentAccountIdChanged() {
|
||||
clearContactSearchBar()
|
||||
}
|
||||
|
||||
// For contact request conv to be focused correctly.
|
||||
function setCurrentUidSmartListModelIndex() {
|
||||
conversationSmartListView.currentIndex
|
||||
= conversationSmartListView.model.currentUidSmartListModelIndex()
|
||||
}
|
||||
|
||||
function updatePendingRequestCount() {
|
||||
pendingRequestCount = UtilsAdapter.getTotalPendingRequest()
|
||||
}
|
||||
|
||||
function updateTotalUnreadMessagesCount() {
|
||||
totalUnreadMessagesCount = UtilsAdapter.getTotalUnreadMessages()
|
||||
function onSelectedContactAdded(convId) {
|
||||
clearContactSearchBar()
|
||||
LRCInstance.selectConversation(convId)
|
||||
}
|
||||
}
|
||||
|
||||
function clearContactSearchBar() {
|
||||
contactSearchBar.clearText()
|
||||
}
|
||||
|
||||
function refreshAccountComboBox(index) {
|
||||
accountComboBox.update()
|
||||
clearContactSearchBar()
|
||||
accountComboBox.resetAccountListModel()
|
||||
}
|
||||
|
||||
function deselectConversationSmartList() {
|
||||
ConversationsAdapter.deselectConversation()
|
||||
conversationSmartListView.currentIndex = -1
|
||||
}
|
||||
|
||||
function forceUpdateConversationSmartListView() {
|
||||
conversationSmartListView.updateListView()
|
||||
}
|
||||
|
||||
function selectTab(tabIndex) {
|
||||
sidePanelTabBar.selectTab(tabIndex)
|
||||
}
|
||||
|
||||
// Intended -> since strange behavior will happen without this for stackview.
|
||||
anchors.top: parent.top
|
||||
anchors.fill: parent
|
||||
|
||||
// Search bar container to embed search label
|
||||
ContactSearchBar {
|
||||
id: contactSearchBar
|
||||
|
||||
|
@ -98,116 +68,114 @@ Rectangle {
|
|||
anchors.rightMargin: 15
|
||||
|
||||
onContactSearchBarTextChanged: {
|
||||
UtilsAdapter.setConversationFilter(text)
|
||||
// not calling positionViewAtBeginning will cause
|
||||
// sort animation visual bugs
|
||||
conversationListView.positionViewAtBeginning()
|
||||
ConversationsAdapter.setFilter(text)
|
||||
}
|
||||
|
||||
onReturnPressedWhileSearching: {
|
||||
var convUid = conversationSmartListView.itemAtIndex(0).convUid()
|
||||
var currentAccountId = AccountAdapter.currentAccountId
|
||||
ConversationsAdapter.selectConversation(currentAccountId, convUid)
|
||||
conversationSmartListView.repositionIndex(convUid)
|
||||
var listView = searchResultsListView.count ?
|
||||
searchResultsListView :
|
||||
conversationListView
|
||||
if (listView.count)
|
||||
listView.model.select(0)
|
||||
}
|
||||
}
|
||||
|
||||
SidePanelTabBar {
|
||||
id: sidePanelTabBar
|
||||
|
||||
visible: ConversationsAdapter.pendingRequestCount &&
|
||||
!contactSearchBar.textContent
|
||||
anchors.top: contactSearchBar.bottom
|
||||
anchors.topMargin: 10
|
||||
anchors.topMargin: visible ? 10 : 0
|
||||
width: sidePanelRect.width
|
||||
height: tabBarVisible ? 42 : 0
|
||||
height: visible ? 42 : 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: searchStatusRect
|
||||
|
||||
visible: lblSearchStatus.text !== ""
|
||||
visible: searchStatusText.text !== ""
|
||||
|
||||
anchors.top: tabBarVisible ? sidePanelTabBar.bottom : contactSearchBar.bottom
|
||||
anchors.topMargin: tabBarVisible ? 0 : 10
|
||||
anchors.top: sidePanelTabBar.bottom
|
||||
anchors.topMargin: visible ? 10 : 0
|
||||
width: parent.width
|
||||
height: 72
|
||||
height: visible ? 42 : 0
|
||||
|
||||
color: "transparent"
|
||||
color: JamiTheme.backgroundColor
|
||||
|
||||
Image {
|
||||
id: searchIcon
|
||||
anchors.left: searchStatusRect.left
|
||||
anchors.leftMargin: 24
|
||||
anchors.verticalCenter: searchStatusRect.verticalCenter
|
||||
width: 24
|
||||
height: 24
|
||||
Text {
|
||||
id: searchStatusText
|
||||
|
||||
layer {
|
||||
enabled: true
|
||||
effect: ColorOverlay {
|
||||
color: JamiTheme.textColor
|
||||
}
|
||||
}
|
||||
|
||||
fillMode: Image.PreserveAspectFit
|
||||
mipmap: true
|
||||
source: "qrc:/images/icons/ic_baseline-search-24px.svg"
|
||||
}
|
||||
|
||||
Label {
|
||||
id: lblSearchStatus
|
||||
|
||||
anchors.verticalCenter: searchStatusRect.verticalCenter
|
||||
anchors.left: searchIcon.right
|
||||
anchors.leftMargin: 24
|
||||
width: searchStatusRect.width - searchIcon.width - 24*2 - 8
|
||||
text: ""
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 32
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 32
|
||||
color: JamiTheme.textColor
|
||||
wrapMode: Text.WordWrap
|
||||
font.pointSize: JamiTheme.menuFontSize
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseAreaSearchRect
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onReleased: {
|
||||
searchStatusRect.color = Qt.binding(function(){return JamiTheme.normalButtonColor})
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
searchStatusRect.color = Qt.binding(function(){return JamiTheme.hoverColor})
|
||||
}
|
||||
|
||||
onExited: {
|
||||
searchStatusRect.color = Qt.binding(function(){return JamiTheme.backgroundColor})
|
||||
}
|
||||
font.pointSize: JamiTheme.filterItemFontSize
|
||||
}
|
||||
}
|
||||
|
||||
ConversationSmartListView {
|
||||
id: conversationSmartListView
|
||||
Connections {
|
||||
target: ConversationsAdapter
|
||||
|
||||
function onShowSearchStatus(status) {
|
||||
searchStatusText.text = status
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: smartListLayout
|
||||
|
||||
anchors.top: searchStatusRect.visible ? searchStatusRect.bottom : (tabBarVisible ? sidePanelTabBar.bottom : contactSearchBar.bottom)
|
||||
anchors.topMargin: (tabBarVisible || searchStatusRect.visible) ? 0 : 10
|
||||
width: parent.width
|
||||
height: tabBarVisible ? sidePanelRect.height - sidePanelTabBar.height - contactSearchBar.height - 20 :
|
||||
sidePanelRect.height - contactSearchBar.height - 20
|
||||
anchors.top: searchStatusRect.bottom
|
||||
anchors.topMargin: (sidePanelTabBar.visible ||
|
||||
searchStatusRect.visible) ? 0 : 12
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
Connections {
|
||||
target: ConversationsAdapter
|
||||
spacing: 4
|
||||
|
||||
function onShowConversationTabs(visible) {
|
||||
tabBarVisible = visible
|
||||
updatePendingRequestCount()
|
||||
updateTotalUnreadMessagesCount()
|
||||
ConversationListView {
|
||||
id: searchResultsListView
|
||||
|
||||
visible: count
|
||||
opacity: visible ? 1 :0
|
||||
|
||||
Layout.topMargin: 10
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: visible ? contentHeight : 0
|
||||
Layout.maximumHeight: {
|
||||
var otherContentHeight = conversationListView.contentHeight + 16
|
||||
if (conversationListView.visible)
|
||||
if (otherContentHeight < parent.height / 2)
|
||||
return parent.height - otherContentHeight
|
||||
else
|
||||
return parent.height / 2
|
||||
else
|
||||
return parent.height
|
||||
}
|
||||
|
||||
function onShowSearchStatus(status) {
|
||||
lblSearchStatus.text = status
|
||||
}
|
||||
model: SearchResultsListModel
|
||||
headerLabel: JamiStrings.searchResults
|
||||
headerVisible: visible
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
ConversationsAdapter.setQmlObject(this)
|
||||
conversationSmartListView.currentIndex = -1
|
||||
ConversationListView {
|
||||
id: conversationListView
|
||||
|
||||
visible: count
|
||||
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.fillHeight: true
|
||||
|
||||
model: ConversationListModel
|
||||
headerLabel: JamiStrings.conversations
|
||||
headerVisible: searchResultsListView.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,31 +28,18 @@ import net.jami.Constants 1.0
|
|||
|
||||
import "../../commoncomponents"
|
||||
|
||||
// TODO:
|
||||
// - totalUnreadMessagesCount and pendingRequestCount could be
|
||||
// properties of ConversationsAdapter
|
||||
// - onCurrentTypeFilterChanged shouldn't need to update the smartlist
|
||||
// - tabBarVisible could be factored out
|
||||
|
||||
TabBar {
|
||||
id: tabBar
|
||||
|
||||
property int currentTypeFilter: ConversationsAdapter.currentTypeFilter
|
||||
|
||||
currentIndex: 0
|
||||
|
||||
enum TabIndex {
|
||||
Conversations,
|
||||
Requests
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ConversationsAdapter
|
||||
|
||||
function onCurrentTypeFilterChanged() {
|
||||
pageOne.down = ConversationsAdapter.currentTypeFilter !== Profile.Type.PENDING
|
||||
pageTwo.down = ConversationsAdapter.currentTypeFilter === Profile.Type.PENDING
|
||||
setCurrentUidSmartListModelIndex()
|
||||
forceReselectConversationSmartListCurrentIndex()
|
||||
}
|
||||
}
|
||||
|
||||
function selectTab(tabIndex) {
|
||||
ConversationsAdapter.currentTypeFilter =
|
||||
(tabIndex === SidePanelTabBar.Conversations) ?
|
||||
|
@ -60,28 +47,25 @@ TabBar {
|
|||
Profile.Type.PENDING
|
||||
}
|
||||
|
||||
visible: tabBarVisible
|
||||
|
||||
currentIndex: 0
|
||||
|
||||
FilterTabButton {
|
||||
id: pageOne
|
||||
id: conversationsTabButton
|
||||
|
||||
down: currentTypeFilter !== Profile.Type.PENDING
|
||||
tabBar: parent
|
||||
down: true
|
||||
labelText: JamiStrings.conversations
|
||||
onSelected: selectTab(SidePanelTabBar.Conversations)
|
||||
badgeCount: totalUnreadMessagesCount
|
||||
badgeCount: ConversationsAdapter.totalUnreadMessageCount
|
||||
acceleratorSequence: "Ctrl+L"
|
||||
}
|
||||
|
||||
FilterTabButton {
|
||||
id: pageTwo
|
||||
id: requestsTabButton
|
||||
|
||||
down: !conversationsTabButton.down
|
||||
tabBar: parent
|
||||
labelText: JamiStrings.invitations
|
||||
onSelected: selectTab(SidePanelTabBar.Requests)
|
||||
badgeCount: pendingRequestCount
|
||||
badgeCount: ConversationsAdapter.pendingRequestCount
|
||||
acceleratorSequence: "Ctrl+R"
|
||||
}
|
||||
}
|
||||
|
|
184
src/mainview/components/SmartListItemDelegate.qml
Normal file
184
src/mainview/components/SmartListItemDelegate.qml
Normal 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)
|
||||
}
|
||||
}
|
|
@ -153,8 +153,6 @@ MessagesAdapter::connectConversationModel()
|
|||
Q_UNUSED(convUid);
|
||||
removeInteraction(interactionId);
|
||||
});
|
||||
|
||||
currentConversationModel->setFilter("");
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -169,7 +167,7 @@ MessagesAdapter::sendContactRequest()
|
|||
void
|
||||
MessagesAdapter::updateConversationForAddedContact()
|
||||
{
|
||||
auto* convModel = lrcInstance_->getCurrentConversationModel();
|
||||
auto convModel = lrcInstance_->getCurrentConversationModel();
|
||||
const auto& convInfo = lrcInstance_->getConversationFromConvUid(
|
||||
lrcInstance_->get_selectedConvUid());
|
||||
|
||||
|
@ -193,7 +191,6 @@ MessagesAdapter::slotSendMessageContentSaved(const QString& content)
|
|||
auto restoredContent = lrcInstance_->getContentDraft(lrcInstance_->get_selectedConvUid(),
|
||||
lrcInstance_->getCurrAccId());
|
||||
setSendMessageContent(restoredContent);
|
||||
Q_EMIT needToUpdateSmartList();
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -202,7 +199,6 @@ MessagesAdapter::slotUpdateDraft(const QString& content)
|
|||
if (!LastConvUid_.isEmpty()) {
|
||||
lrcInstance_->setContentDraft(LastConvUid_, lrcInstance_->getCurrAccId(), content);
|
||||
}
|
||||
Q_EMIT needToUpdateSmartList();
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -460,11 +456,9 @@ MessagesAdapter::setConversationProfileData(const lrc::api::conversation::Info&
|
|||
try {
|
||||
auto& contact = accInfo->contactModel->getContact(contactUri);
|
||||
auto bestName = accInfo->contactModel->bestNameForContact(contactUri);
|
||||
setInvitation(contact.profileInfo.type == lrc::api::profile::Type::PENDING
|
||||
|| contact.profileInfo.type == lrc::api::profile::Type::TEMPORARY,
|
||||
setInvitation(contact.profileInfo.type == lrc::api::profile::Type::PENDING,
|
||||
bestName,
|
||||
contactUri);
|
||||
|
||||
if (!contact.profileInfo.avatar.isEmpty()) {
|
||||
setSenderImage(contactUri, contact.profileInfo.avatar);
|
||||
} else {
|
||||
|
|
|
@ -92,7 +92,6 @@ protected:
|
|||
void contactIsComposing(const QString& convUid, const QString& contactUri, bool isComposing);
|
||||
|
||||
Q_SIGNALS:
|
||||
void needToUpdateSmartList();
|
||||
void contactBanned();
|
||||
void navigateToWelcomePageRequested();
|
||||
void invitationAccepted();
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
#include "moderatorlistmodel.h"
|
||||
#include "deviceitemlistmodel.h"
|
||||
#include "smartlistmodel.h"
|
||||
#include "conversationlistmodelbase.h"
|
||||
|
||||
#include "appsettingsmanager.h"
|
||||
#include "distantrenderer.h"
|
||||
|
@ -106,6 +107,10 @@ registerTypes()
|
|||
QML_REGISTERTYPE(NS_MODELS, PluginListPreferenceModel);
|
||||
QML_REGISTERTYPE(NS_MODELS, SmartListModel);
|
||||
|
||||
// Roles & type enums for models
|
||||
QML_REGISTERNAMESPACE(NS_MODELS, ConversationList::staticMetaObject, "ConversationList");
|
||||
QML_REGISTERNAMESPACE(NS_MODELS, ContactList::staticMetaObject, "ContactList");
|
||||
|
||||
// QQuickItems
|
||||
QML_REGISTERTYPE(NS_MODELS, PreviewRenderer);
|
||||
QML_REGISTERTYPE(NS_MODELS, VideoCallPreviewRenderer);
|
||||
|
|
|
@ -42,13 +42,12 @@ Q_CLASSINFO("RegisterEnumClassesUnscoped", "false")
|
|||
QQmlEngine::setObjectOwnership(I, QQmlEngine::CppOwnership); \
|
||||
{ using T = std::remove_reference<decltype(*I)>::type; \
|
||||
qmlRegisterSingletonType<T>(NS, VER_MAJ, VER_MIN, N, \
|
||||
[I](QQmlEngine*, QJSEngine*) -> QObject* { \
|
||||
return I; }); }
|
||||
[i=I](QQmlEngine*, QJSEngine*) -> QObject* { \
|
||||
return i; }); }
|
||||
|
||||
#define QML_REGISTERSINGLETONTYPE_CUSTOM(NS, T, P) \
|
||||
qmlRegisterSingletonType<T>(NS, VER_MAJ, VER_MIN, #T, \
|
||||
[p=P](QQmlEngine* e, QJSEngine* se) -> QObject* { \
|
||||
Q_UNUSED(e); Q_UNUSED(se); \
|
||||
[p=P](QQmlEngine*, QJSEngine*) -> QObject* { \
|
||||
return p; \
|
||||
});
|
||||
// clang-format on
|
||||
|
|
57
src/searchresultslistmodel.cpp
Normal file
57
src/searchresultslistmodel.cpp
Normal 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();
|
||||
}
|
49
src/searchresultslistmodel.h
Normal file
49
src/searchresultslistmodel.h
Normal 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) {};
|
||||
};
|
167
src/selectablelistproxymodel.cpp
Normal file
167
src/selectablelistproxymodel.cpp
Normal 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
66
src/selectablelistproxymodel.h
Normal file
66
src/selectablelistproxymodel.h
Normal 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_;
|
||||
};
|
|
@ -232,7 +232,7 @@ ColumnLayout {
|
|||
|
||||
onClicked: {
|
||||
ContactPickerCreation.createContactPickerObjects(
|
||||
ContactPicker.ContactPickerType.CONVERSATION,
|
||||
ContactList.CONVERSATION,
|
||||
mainView)
|
||||
ContactPickerCreation.openContactPicker()
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ Rectangle {
|
|||
id: deleteAccountDialog
|
||||
|
||||
onAccepted: {
|
||||
AccountAdapter.setSelectedConvId()
|
||||
LRCInstance.deselectConversation()
|
||||
|
||||
if(UtilsAdapter.getAccountListSize() > 0) {
|
||||
navigateToMainView()
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*!
|
||||
/*
|
||||
* Copyright (C) 2017-2020 by Savoir-faire Linux
|
||||
* Author: Anthony Léonard <anthony.leonard@savoirfairelinux.com>
|
||||
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com>
|
||||
|
@ -34,17 +34,14 @@
|
|||
SmartListModel::SmartListModel(QObject* parent,
|
||||
SmartListModel::Type listModelType,
|
||||
LRCInstance* instance)
|
||||
: AbstractListModelBase(parent)
|
||||
: ConversationListModelBase(instance, parent)
|
||||
, listModelType_(listModelType)
|
||||
{
|
||||
lrcInstance_ = instance;
|
||||
if (listModelType_ == Type::CONFERENCE) {
|
||||
setConferenceableFilter();
|
||||
}
|
||||
}
|
||||
|
||||
SmartListModel::~SmartListModel() {}
|
||||
|
||||
int
|
||||
SmartListModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
|
@ -70,102 +67,75 @@ SmartListModel::rowCount(const QModelIndex& parent) const
|
|||
return 0;
|
||||
}
|
||||
|
||||
int
|
||||
SmartListModel::columnCount(const QModelIndex& parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
QVariant
|
||||
SmartListModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return QVariant();
|
||||
}
|
||||
if (!index.isValid())
|
||||
return {};
|
||||
|
||||
try {
|
||||
auto& currentAccountInfo = lrcInstance_->accountModel().getAccountInfo(
|
||||
lrcInstance_->getCurrAccId());
|
||||
auto& convModel = currentAccountInfo.conversationModel;
|
||||
if (listModelType_ == Type::TRANSFER) {
|
||||
switch (listModelType_) {
|
||||
case Type::TRANSFER: {
|
||||
try {
|
||||
auto& currentAccountInfo = lrcInstance_->accountModel().getAccountInfo(
|
||||
lrcInstance_->getCurrAccId());
|
||||
auto& convModel = currentAccountInfo.conversationModel;
|
||||
auto filterType = currentAccountInfo.profileInfo.type;
|
||||
auto& item = convModel->getFilteredConversations(filterType).at(index.row());
|
||||
return getConversationItemData(item, currentAccountInfo, role);
|
||||
} else if (listModelType_ == Type::CONFERENCE) {
|
||||
auto calls = conferenceables_[ConferenceableItem::CALL];
|
||||
auto contacts = conferenceables_[ConferenceableItem::CONTACT];
|
||||
QString itemConvUid {}, itemAccountId {};
|
||||
if (calls.size() == 0) {
|
||||
itemConvUid = contacts.at(index.row()).at(0).convId;
|
||||
itemAccountId = contacts.at(index.row()).at(0).accountId;
|
||||
} else {
|
||||
bool callsOpen = sectionState_[tr("Calls")];
|
||||
bool contactsOpen = sectionState_[tr("Contacts")];
|
||||
auto callSectionEnd = callsOpen ? calls.size() + 1 : 1;
|
||||
auto contactSectionEnd = contactsOpen ? callSectionEnd + contacts.size() + 1
|
||||
: callSectionEnd + 1;
|
||||
if (index.row() < callSectionEnd) {
|
||||
if (index.row() == 0) {
|
||||
return QVariant(role == Role::SectionName
|
||||
? (callsOpen ? "➖ " : "➕ ") + QString(tr("Calls"))
|
||||
: "");
|
||||
} else {
|
||||
auto idx = index.row() - 1;
|
||||
itemConvUid = calls.at(idx).at(0).convId;
|
||||
itemAccountId = calls.at(idx).at(0).accountId;
|
||||
}
|
||||
} else if (index.row() < contactSectionEnd) {
|
||||
if (index.row() == callSectionEnd) {
|
||||
return QVariant(role == Role::SectionName
|
||||
? (contactsOpen ? "➖ " : "➕ ") + QString(tr("Contacts"))
|
||||
: "");
|
||||
} else {
|
||||
auto idx = index.row() - (callSectionEnd + 1);
|
||||
itemConvUid = contacts.at(idx).at(0).convId;
|
||||
itemAccountId = contacts.at(idx).at(0).accountId;
|
||||
}
|
||||
return dataForItem(item, role);
|
||||
} catch (const std::exception& e) {
|
||||
qWarning() << e.what();
|
||||
}
|
||||
} break;
|
||||
case Type::CONFERENCE: {
|
||||
auto calls = conferenceables_[ConferenceableItem::CALL];
|
||||
auto contacts = conferenceables_[ConferenceableItem::CONTACT];
|
||||
QString itemConvUid {}, itemAccountId {};
|
||||
if (calls.size() == 0) {
|
||||
itemConvUid = contacts.at(index.row()).at(0).convId;
|
||||
itemAccountId = contacts.at(index.row()).at(0).accountId;
|
||||
} else {
|
||||
bool callsOpen = sectionState_[tr("Calls")];
|
||||
bool contactsOpen = sectionState_[tr("Contacts")];
|
||||
auto callSectionEnd = callsOpen ? calls.size() + 1 : 1;
|
||||
auto contactSectionEnd = contactsOpen ? callSectionEnd + contacts.size() + 1
|
||||
: callSectionEnd + 1;
|
||||
if (index.row() < callSectionEnd) {
|
||||
if (index.row() == 0) {
|
||||
return QVariant(role == Role::SectionName
|
||||
? (callsOpen ? "➖ " : "➕ ") + QString(tr("Calls"))
|
||||
: "");
|
||||
} else {
|
||||
auto idx = index.row() - 1;
|
||||
itemConvUid = calls.at(idx).at(0).convId;
|
||||
itemAccountId = calls.at(idx).at(0).accountId;
|
||||
}
|
||||
} else if (index.row() < contactSectionEnd) {
|
||||
if (index.row() == callSectionEnd) {
|
||||
return QVariant(role == Role::SectionName
|
||||
? (contactsOpen ? "➖ " : "➕ ") + QString(tr("Contacts"))
|
||||
: "");
|
||||
} else {
|
||||
auto idx = index.row() - (callSectionEnd + 1);
|
||||
itemConvUid = contacts.at(idx).at(0).convId;
|
||||
itemAccountId = contacts.at(idx).at(0).accountId;
|
||||
}
|
||||
}
|
||||
if (role == Role::AccountId) {
|
||||
return QVariant(itemAccountId);
|
||||
}
|
||||
|
||||
auto& itemAccountInfo = lrcInstance_->accountModel().getAccountInfo(itemAccountId);
|
||||
auto& item = lrcInstance_->getConversationFromConvUid(itemConvUid, itemAccountId);
|
||||
return getConversationItemData(item, itemAccountInfo, role);
|
||||
} else if (listModelType_ == Type::CONVERSATION) {
|
||||
auto& item = conversations_.at(index.row());
|
||||
return getConversationItemData(item, currentAccountInfo, role);
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
qWarning() << e.what();
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
if (role == Role::AccountId) {
|
||||
return QVariant(itemAccountId);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray>
|
||||
SmartListModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[DisplayName] = "DisplayName";
|
||||
roles[DisplayID] = "DisplayID";
|
||||
roles[Presence] = "Presence";
|
||||
roles[URI] = "URI";
|
||||
roles[UnreadMessagesCount] = "UnreadMessagesCount";
|
||||
roles[LastInteractionDate] = "LastInteractionDate";
|
||||
roles[LastInteraction] = "LastInteraction";
|
||||
roles[ContactType] = "ContactType";
|
||||
roles[UID] = "UID";
|
||||
roles[InCall] = "InCall";
|
||||
roles[IsAudioOnly] = "IsAudioOnly";
|
||||
roles[CallStackViewShouldShow] = "CallStackViewShouldShow";
|
||||
roles[CallState] = "CallState";
|
||||
roles[SectionName] = "SectionName";
|
||||
roles[AccountId] = "AccountId";
|
||||
roles[Draft] = "Draft";
|
||||
roles[PictureUid] = "PictureUid";
|
||||
return roles;
|
||||
auto& item = lrcInstance_->getConversationFromConvUid(itemConvUid, itemAccountId);
|
||||
return dataForItem(item, role);
|
||||
} break;
|
||||
case Type::CONVERSATION: {
|
||||
auto& item = conversations_.at(index.row());
|
||||
return dataForItem(item, role);
|
||||
} break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -194,39 +164,6 @@ SmartListModel::fillConversationsList()
|
|||
endResetModel();
|
||||
}
|
||||
|
||||
void
|
||||
SmartListModel::updateContactAvatarUid(const QString& contactUri)
|
||||
{
|
||||
contactAvatarUidMap_[contactUri] = Utils::generateUid();
|
||||
}
|
||||
|
||||
void
|
||||
SmartListModel::fillContactAvatarUidMap(const ContactModel::ContactInfoMap& contacts)
|
||||
{
|
||||
if (contacts.size() == 0) {
|
||||
contactAvatarUidMap_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (contactAvatarUidMap_.isEmpty() || contacts.size() != contactAvatarUidMap_.size()) {
|
||||
bool useContacts = contacts.size() > contactAvatarUidMap_.size();
|
||||
auto contactsKeyList = contacts.keys();
|
||||
auto contactAvatarUidMapKeyList = contactAvatarUidMap_.keys();
|
||||
|
||||
for (int i = 0;
|
||||
i < (useContacts ? contactsKeyList.size() : contactAvatarUidMapKeyList.size());
|
||||
++i) {
|
||||
// Insert or update
|
||||
if (i < contactsKeyList.size() && !contactAvatarUidMap_.contains(contactsKeyList.at(i)))
|
||||
contactAvatarUidMap_.insert(contactsKeyList.at(i), Utils::generateUid());
|
||||
// Remove
|
||||
if (i < contactAvatarUidMapKeyList.size()
|
||||
&& !contacts.contains(contactAvatarUidMapKeyList.at(i)))
|
||||
contactAvatarUidMap_.remove(contactAvatarUidMapKeyList.at(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
SmartListModel::toggleSection(const QString& section)
|
||||
{
|
||||
|
@ -251,140 +188,6 @@ SmartListModel::currentUidSmartListModelIndex()
|
|||
return -1;
|
||||
}
|
||||
|
||||
QVariant
|
||||
SmartListModel::getConversationItemData(const conversation::Info& item,
|
||||
const account::Info& accountInfo,
|
||||
int role) const
|
||||
{
|
||||
if (item.participants.size() <= 0) {
|
||||
return QVariant();
|
||||
}
|
||||
auto& contactModel = accountInfo.contactModel;
|
||||
|
||||
// Since we are using image provider right now, image url representation should be unique to
|
||||
// be able to use the image cache, account avatar will only be updated once PictureUid changed
|
||||
switch (role) {
|
||||
case Role::DisplayName: {
|
||||
if (!item.participants.isEmpty())
|
||||
return QVariant(contactModel->bestNameForContact(item.participants[0]));
|
||||
return QVariant("");
|
||||
}
|
||||
case Role::DisplayID: {
|
||||
if (!item.participants.isEmpty())
|
||||
return QVariant(contactModel->bestIdForContact(item.participants[0]));
|
||||
return QVariant("");
|
||||
}
|
||||
case Role::Presence: {
|
||||
if (!item.participants.isEmpty()) {
|
||||
auto& contact = contactModel->getContact(item.participants[0]);
|
||||
return QVariant(contact.isPresent);
|
||||
}
|
||||
return QVariant(false);
|
||||
}
|
||||
case Role::PictureUid: {
|
||||
if (!item.participants.isEmpty()) {
|
||||
return QVariant(contactAvatarUidMap_[item.participants[0]]);
|
||||
}
|
||||
return QVariant("");
|
||||
}
|
||||
case Role::URI: {
|
||||
if (!item.participants.isEmpty()) {
|
||||
return QVariant(item.participants[0]);
|
||||
}
|
||||
return QVariant("");
|
||||
}
|
||||
case Role::UnreadMessagesCount:
|
||||
return QVariant(item.unreadMessages);
|
||||
case Role::LastInteractionDate: {
|
||||
if (!item.interactions.empty()) {
|
||||
auto& date = item.interactions.at(item.lastMessageUid).timestamp;
|
||||
return QVariant(Utils::formatTimeString(date));
|
||||
}
|
||||
return QVariant("");
|
||||
}
|
||||
case Role::LastInteraction: {
|
||||
if (!item.interactions.empty()) {
|
||||
return QVariant(item.interactions.at(item.lastMessageUid).body);
|
||||
}
|
||||
return QVariant("");
|
||||
}
|
||||
case Role::LastInteractionType: {
|
||||
if (!item.interactions.empty()) {
|
||||
return QVariant(static_cast<int>(item.interactions.at(item.lastMessageUid).type));
|
||||
}
|
||||
return QVariant(0);
|
||||
}
|
||||
case Role::ContactType: {
|
||||
if (!item.participants.isEmpty()) {
|
||||
auto& contact = contactModel->getContact(item.participants[0]);
|
||||
return QVariant(static_cast<int>(contact.profileInfo.type));
|
||||
}
|
||||
return QVariant(0);
|
||||
}
|
||||
case Role::UID:
|
||||
return QVariant(item.uid);
|
||||
case Role::InCall: {
|
||||
const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
|
||||
if (!convInfo.uid.isEmpty()) {
|
||||
auto* callModel = lrcInstance_->getCurrentCallModel();
|
||||
return QVariant(callModel->hasCall(convInfo.callId));
|
||||
}
|
||||
return QVariant(false);
|
||||
}
|
||||
case Role::IsAudioOnly: {
|
||||
const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
|
||||
if (!convInfo.uid.isEmpty()) {
|
||||
auto* call = lrcInstance_->getCallInfoForConversation(convInfo);
|
||||
if (call) {
|
||||
return QVariant(call->isAudioOnly);
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
case Role::CallStackViewShouldShow: {
|
||||
const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
|
||||
if (!convInfo.uid.isEmpty() && !convInfo.callId.isEmpty()) {
|
||||
auto* callModel = lrcInstance_->getCurrentCallModel();
|
||||
const auto& call = callModel->getCall(convInfo.callId);
|
||||
return QVariant(
|
||||
callModel->hasCall(convInfo.callId)
|
||||
&& ((!call.isOutgoing
|
||||
&& (call.status == lrc::api::call::Status::IN_PROGRESS
|
||||
|| call.status == lrc::api::call::Status::PAUSED
|
||||
|| call.status == lrc::api::call::Status::INCOMING_RINGING))
|
||||
|| (call.isOutgoing && call.status != lrc::api::call::Status::ENDED)));
|
||||
}
|
||||
return QVariant(false);
|
||||
}
|
||||
case Role::CallState: {
|
||||
const auto& convInfo = lrcInstance_->getConversationFromConvUid(item.uid);
|
||||
if (!convInfo.uid.isEmpty()) {
|
||||
if (auto* call = lrcInstance_->getCallInfoForConversation(convInfo)) {
|
||||
return QVariant(static_cast<int>(call->status));
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
case Role::SectionName:
|
||||
return QVariant(QString());
|
||||
case Role::Draft: {
|
||||
if (!item.uid.isEmpty()) {
|
||||
const auto draft = lrcInstance_->getContentDraft(item.uid, accountInfo.id);
|
||||
if (!draft.isEmpty()) {
|
||||
/*
|
||||
* Pencil Emoji
|
||||
*/
|
||||
uint cp = 0x270F;
|
||||
auto emojiString = QString::fromUcs4(&cp, 1);
|
||||
return emojiString + lrcInstance_->getContentDraft(item.uid, accountInfo.id);
|
||||
}
|
||||
}
|
||||
return QVariant("");
|
||||
}
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QModelIndex
|
||||
SmartListModel::index(int row, int column, const QModelIndex& parent) const
|
||||
{
|
||||
|
@ -399,13 +202,6 @@ SmartListModel::index(int row, int column, const QModelIndex& parent) const
|
|||
return QModelIndex();
|
||||
}
|
||||
|
||||
QModelIndex
|
||||
SmartListModel::parent(const QModelIndex& child) const
|
||||
{
|
||||
Q_UNUSED(child);
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
Qt::ItemFlags
|
||||
SmartListModel::flags(const QModelIndex& index) const
|
||||
{
|
||||
|
|
|
@ -20,60 +20,32 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "abstractlistmodelbase.h"
|
||||
#include "conversationlistmodelbase.h"
|
||||
|
||||
namespace ContactList {
|
||||
Q_NAMESPACE
|
||||
enum Type { CONVERSATION, CONFERENCE, TRANSFER, COUNT__ };
|
||||
Q_ENUM_NS(Type)
|
||||
} // namespace ContactList
|
||||
|
||||
using namespace lrc::api;
|
||||
class LRCInstance;
|
||||
|
||||
class SmartListModel : public AbstractListModelBase
|
||||
class SmartListModel : public ConversationListModelBase
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
using AccountInfo = lrc::api::account::Info;
|
||||
using ConversationInfo = lrc::api::conversation::Info;
|
||||
using ContactInfo = lrc::api::contact::Info;
|
||||
|
||||
enum class Type { CONVERSATION, CONFERENCE, TRANSFER, COUNT__ };
|
||||
|
||||
enum Role {
|
||||
DisplayName = Qt::UserRole + 1,
|
||||
DisplayID,
|
||||
Presence,
|
||||
URI,
|
||||
UnreadMessagesCount,
|
||||
LastInteractionDate,
|
||||
LastInteraction,
|
||||
LastInteractionType,
|
||||
ContactType,
|
||||
UID,
|
||||
ContextMenuOpen,
|
||||
InCall,
|
||||
IsAudioOnly,
|
||||
CallStackViewShouldShow,
|
||||
CallState,
|
||||
SectionName,
|
||||
AccountId,
|
||||
PictureUid,
|
||||
Draft
|
||||
};
|
||||
Q_ENUM(Role)
|
||||
using Type = ContactList::Type;
|
||||
|
||||
explicit SmartListModel(QObject* parent = nullptr,
|
||||
SmartListModel::Type listModelType = Type::CONVERSATION,
|
||||
Type listModelType = Type::CONVERSATION,
|
||||
LRCInstance* instance = nullptr);
|
||||
~SmartListModel();
|
||||
|
||||
/*
|
||||
* QAbstractListModel.
|
||||
*/
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
int columnCount(const QModelIndex& parent) const override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
QModelIndex index(int row,
|
||||
int column = 0,
|
||||
const QModelIndex& parent = QModelIndex()) const override;
|
||||
QModelIndex parent(const QModelIndex& child) const override;
|
||||
Qt::ItemFlags flags(const QModelIndex& index) const override;
|
||||
|
||||
Q_INVOKABLE void setConferenceableFilter(const QString& filter = {});
|
||||
|
@ -81,28 +53,9 @@ public:
|
|||
Q_INVOKABLE int currentUidSmartListModelIndex();
|
||||
Q_INVOKABLE void fillConversationsList();
|
||||
|
||||
/*
|
||||
* This function is to update contact avatar uuid for current account when there's an contact
|
||||
* avatar changed.
|
||||
*/
|
||||
Q_INVOKABLE void updateContactAvatarUid(const QString& contactUri);
|
||||
|
||||
private:
|
||||
QVariant getConversationItemData(const ConversationInfo& item,
|
||||
const AccountInfo& accountInfo,
|
||||
int role) const;
|
||||
|
||||
/*
|
||||
* Give a uuid for each contact avatar for current account and it will serve PictureUid role
|
||||
*/
|
||||
void fillContactAvatarUidMap(const ContactModel::ContactInfoMap& contacts);
|
||||
|
||||
/*
|
||||
* List sectioning.
|
||||
*/
|
||||
Type listModelType_;
|
||||
QMap<QString, bool> sectionState_;
|
||||
QMap<ConferenceableItem, ConferenceableValue> conferenceables_;
|
||||
QMap<QString, QString> contactAvatarUidMap_;
|
||||
ConversationModel::ConversationQueueProxy conversations_;
|
||||
};
|
||||
|
|
|
@ -501,9 +501,9 @@ Utils::formatTimeString(const std::time_t& timeStamp)
|
|||
{
|
||||
auto currentTimeStamp = QDateTime::fromSecsSinceEpoch(timeStamp);
|
||||
auto now = QDateTime::currentDateTime();
|
||||
auto timeStampDMY = currentTimeStamp.toString("dd/MM/yyyy");
|
||||
if (timeStampDMY == now.toString("dd/MM/yyyy")) {
|
||||
return currentTimeStamp.toString("hh:mm");
|
||||
auto timeStampDMY = currentTimeStamp.toString("dd/MM/yy");
|
||||
if (timeStampDMY == now.toString("dd/MM/yy")) {
|
||||
return currentTimeStamp.toString("hhmm");
|
||||
} else {
|
||||
return timeStampDMY;
|
||||
}
|
||||
|
|
|
@ -144,29 +144,6 @@ UtilsAdapter::getBestId(const QString& accountId, const QString& uid)
|
|||
return QString();
|
||||
}
|
||||
|
||||
int
|
||||
UtilsAdapter::getTotalUnreadMessages()
|
||||
{
|
||||
int totalUnreadMessages {0};
|
||||
if (lrcInstance_->getCurrentAccountInfo().profileInfo.type != lrc::api::profile::Type::SIP) {
|
||||
auto* convModel = lrcInstance_->getCurrentConversationModel();
|
||||
auto ringConversations = convModel->getFilteredConversations(lrc::api::profile::Type::RING,
|
||||
false);
|
||||
ringConversations.for_each(
|
||||
[&totalUnreadMessages](const lrc::api::conversation::Info& conversation) {
|
||||
totalUnreadMessages += conversation.unreadMessages;
|
||||
});
|
||||
}
|
||||
return totalUnreadMessages;
|
||||
}
|
||||
|
||||
int
|
||||
UtilsAdapter::getTotalPendingRequest()
|
||||
{
|
||||
auto& accountInfo = lrcInstance_->getCurrentAccountInfo();
|
||||
return accountInfo.contactModel->pendingRequestCount();
|
||||
}
|
||||
|
||||
void
|
||||
UtilsAdapter::setConversationFilter(const QString& filter)
|
||||
{
|
||||
|
|
|
@ -49,8 +49,6 @@ public:
|
|||
Q_INVOKABLE QString GetRingtonePath();
|
||||
Q_INVOKABLE bool checkStartupLink();
|
||||
Q_INVOKABLE void setConversationFilter(const QString& filter);
|
||||
Q_INVOKABLE int getTotalUnreadMessages();
|
||||
Q_INVOKABLE int getTotalPendingRequest();
|
||||
Q_INVOKABLE const QString getBestName(const QString& accountId, const QString& uid);
|
||||
Q_INVOKABLE const QString getPeerUri(const QString& accountId, const QString& uid);
|
||||
Q_INVOKABLE QString getBestId(const QString& accountId);
|
||||
|
|
Loading…
Add table
Reference in a new issue