Improve Contacts: sorting, searching and unread

ChatHandler::handleUnreadMsgs(...) deprecate 'id' field
  in favour of 'chat_id'
IdentityHandler::handleWildcard(...) reduce sleep time from 500 to 50 ms
IdentityHandler::handleWildcard(...) expose 'is_contact' to JSON API
Split contacts list delegate into GxsIdentityDelegate.qml
Update to QtQuick.Controls 2.0 because 1.4 is not supported anymore and
  2.1 is not available yet in Qt 5.7.1
ChatView.qml mark messages as read
Set refreshTimer.triggeredOnStart=true to improve reponsiveness of views
Contacts.qml use a Popup that is available in Controls 2.0 instead of a
  castrated Dialog to display full fingerprint
TrustedNodesView.qml check if locations[*].is_online is an array before
  attempting calling reduce
main.qml added menuentry to shutdown de core
main.qml added menuantry to search contact
This commit is contained in:
Gioacchino Mazzurco 2017-03-24 12:02:13 +01:00
parent 34dc1fac37
commit abe84a4f81
17 changed files with 409 additions and 141 deletions

View File

@ -924,7 +924,7 @@ ResponseTask* ChatHandler::handleLobbyParticipants(Request &req, Response &resp)
void ChatHandler::handleMessages(Request &req, Response &resp)
{
/* G10h4ck: Whithout this the request processing won't happen, copied from
* ChatHandler::handleLobbies, is this a work around or is the right whay of
* ChatHandler::handleLobbies, is this a work around or is the right way of
* doing it? */
tick();
@ -1118,7 +1118,8 @@ void ChatHandler::handleUnreadMsgs(Request &/*req*/, Response &resp)
RS_STACK_MUTEX(mMtx); /********** LOCKED **********/
resp.mDataStream.getStreamToMember();
for(std::map<ChatId, std::list<Msg> >::const_iterator mit = mMsgs.begin(); mit != mMsgs.end(); ++mit)
for( std::map<ChatId, std::list<Msg> >::const_iterator mit = mMsgs.begin();
mit != mMsgs.end(); ++mit )
{
uint32_t count = 0;
for(std::list<Msg>::const_iterator lit = mit->second.begin(); lit != mit->second.end(); ++lit)
@ -1130,7 +1131,10 @@ void ChatHandler::handleUnreadMsgs(Request &/*req*/, Response &resp)
if(count && (mit2 != mChatInfo.end()))
{
resp.mDataStream.getStreamToMember()
<< makeKeyValue("id", mit->first.toStdString())
#warning @deprecated using "id" as key can cause problems in some JS based \
languages like Qml @see chat_id instead
<< makeKeyValue("id", mit->first.toStdString())
<< makeKeyValue("chat_id", mit->first.toStdString())
<< makeKeyValueReference("unread_count", count)
<< mit2->second;
}
@ -1162,7 +1166,8 @@ void ChatHandler::handleInitiateDistantChatConnexion(Request& req, Response& res
DistantChatPeerId distant_chat_id;
uint32_t error_code;
if(mRsMsgs->initiateDistantChatConnexion(receiver_id, sender_id, distant_chat_id, error_code))
if(mRsMsgs->initiateDistantChatConnexion(receiver_id, sender_id,
distant_chat_id, error_code))
resp.setOk();
else resp.setFail("Failed to initiate distant chat");

View File

@ -61,7 +61,9 @@ protected:
{
case BEGIN:{
RsIdentityParameters params;
req.mStream << makeKeyValueReference("name", params.nickname) << makeKeyValueReference("pgp_linked", params.isPgpLinked);
req.mStream
<< makeKeyValueReference("name", params.nickname)
<< makeKeyValueReference("pgp_linked", params.isPgpLinked);
if(params.nickname == "")
{
@ -141,9 +143,9 @@ void IdentityHandler::handleWildcard(Request & /*req*/, Response &resp)
)
{
#ifdef WINDOWS_SYS
Sleep(500);
Sleep(50);
#else
usleep(500*1000);
usleep(50*1000);
#endif
}
@ -156,14 +158,18 @@ void IdentityHandler::handleWildcard(Request & /*req*/, Response &resp)
RsGxsIdGroup& grp = *vit;
//electron: not very happy about this, i think the flags should stay hidden in rsidentities
bool own = (grp.mMeta.mSubscribeFlags & GXS_SERV::GROUP_SUBSCRIBE_ADMIN);
bool pgp_linked = (grp.mMeta.mGroupFlags & RSGXSID_GROUPFLAG_REALID_kept_for_compatibility ) ;
resp.mDataStream.getStreamToMember()
<< makeKeyValueReference("id", grp.mMeta.mGroupId) /// @deprecated using "id" as key can cause problems in some JS based languages like Qml @see gxs_id instead
bool pgp_linked = (grp.mMeta.mGroupFlags &
RSGXSID_GROUPFLAG_REALID_kept_for_compatibility);
resp.mDataStream.getStreamToMember()
#warning @deprecated using "id" as key can cause problems in some JS based \
languages like Qml @see gxs_id instead
<< makeKeyValueReference("id", grp.mMeta.mGroupId)
<< makeKeyValueReference("gxs_id", grp.mMeta.mGroupId)
<< makeKeyValueReference("pgp_id",grp.mPgpId )
<< makeKeyValueReference("name", grp.mMeta.mGroupName)
<< makeKeyValueReference("own", own)
<< makeKeyValueReference("pgp_linked", pgp_linked);
<< makeKeyValueReference("pgp_linked", pgp_linked)
<< makeKeyValueReference("is_contact", grp.mIsAContact);
}
}
else ok = false;

View File

@ -91,11 +91,11 @@ class GxsReputation
};
class RsGxsIdGroup
struct RsGxsIdGroup
{
public:
RsGxsIdGroup(): mLastUsageTS(0), mPgpKnown(false),mIsAContact(false) { return; }
~RsGxsIdGroup() { return; }
RsGxsIdGroup() :
mLastUsageTS(0), mPgpKnown(false), mIsAContact(false) {}
~RsGxsIdGroup() {}
RsGroupMetaData mMeta;

View File

@ -13,5 +13,10 @@
<file>qml/icons/remove-link.png</file>
<file>qml/icons/state-offline.png</file>
<file>qml/icons/state-ok.png</file>
<file>qml/GxsIdentityDelegate.qml</file>
<file>qml/icons/edit-find.png</file>
<file>qml/icons/edit-image-face-detect.png</file>
<file>qml/icons/document-share.png</file>
<file>qml/icons/application-menu.png</file>
</qresource>
</RCC>

View File

@ -1,5 +1,5 @@
import QtQuick 2.0
import QtQuick.Controls 1.4
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import org.retroshare.qml_components.LibresapiLocalClient 1.0

View File

@ -17,7 +17,7 @@
*/
import QtQuick 2.0
import QtQuick.Controls 1.4
import QtQuick.Controls 2.0
import org.retroshare.qml_components.LibresapiLocalClient 1.0
Item
@ -27,8 +27,12 @@ Item
function refreshData()
{
rsApi.request( "/chat/messages/"+ chatId, "",
function(par) { chatModel.json = par.response } )
rsApi.request( "/chat/messages/"+chatId, "", function(par)
{
chatModel.json = par.response
if(visible) rsApi.request("/chat/mark_chat_as_read/"+chatId, "",
null)
} )
}
onFocusChanged: focus && refreshData()
@ -89,6 +93,7 @@ Item
id: refreshTimer
interval: 800
repeat: true
triggeredOnStart: true
onTriggered: if(chatView.visible) chatView.refreshData()
Component.onCompleted: start()
}

View File

@ -17,8 +17,9 @@
*/
import QtQuick 2.0
import QtQuick.Controls 1.4
import QtQuick.Controls 2.0
import QtQuick.Dialogs 1.2
import QtQml.Models 2.2
import org.retroshare.qml_components.LibresapiLocalClient 1.0
Item
@ -26,6 +27,8 @@ Item
id: contactsView
property string own_gxs_id: ""
property string own_nick: ""
property bool searching: false
property var unreadMessages: ({})
Component.onCompleted: refreshOwn()
@ -33,7 +36,7 @@ Item
{
function refreshCallback(par)
{
locationsModel.json = par.response
gxsIdsModel.json = par.response
if(contactsView.own_gxs_id == "") refreshOwn()
}
rsApi.request("/identity/*/", "", refreshCallback)
@ -54,113 +57,174 @@ Item
function startChatCallback(par)
{
var chId = JSON.parse(par.response).data.chat_id
stackView.push({
item:"qrc:/qml/ChatView.qml",
properties: {chatId: chId}
})
stackView.push("qrc:/qml/ChatView.qml", {'chatId': chId})
}
function refreshUnread()
{
rsApi.request("/chat/unread_msgs", "", function(par)
{
var jsonData = JSON.parse(par.response).data
var dataLen = jsonData.length
if(JSON.stringify(unreadMessages) != JSON.stringify(jsonData))
{
unreadMessages = {}
for ( var i=0; i<dataLen; ++i)
{
var el = jsonData[i]
if(el.is_distant_chat_id)
unreadMessages[el.remote_author_id] = el.unread_count
}
visualModel.resetSorting()
}
})
}
/** This must be equivalent to
p3GxsTunnelService::makeGxsTunnelId(...) */
function getChatId(from_gxs, to_gxs)
{
return from_gxs < to_gxs ? from_gxs + to_gxs : to_gxs + from_gxs
}
onFocusChanged: focus && refreshData()
JSONListModel
{
id: locationsModel
id: gxsIdsModel
query: "$.data[*]"
}
DelegateModel
{
/* More documentation about this is available at:
* http://doc.qt.io/qt-5/qml-qtqml-models-delegatemodel.html
* http://doc.qt.io/qt-5/qtquick-tutorials-dynamicview-dynamicview4-example.html
* http://imaginativethinking.ca/use-qt-quicks-delegatemodelgroup/
*/
id: visualModel
model: gxsIdsModel.model
property var lessThan:
[
function(left, right)
{
var lfun = unreadMessages.hasOwnProperty(left.gxs_id) ?
unreadMessages[left.gxs_id] : 0
var rgun = unreadMessages.hasOwnProperty(right.gxs_id) ?
unreadMessages[right.gxs_id] : 0
if( lfun !== rgun ) return lfun > rgun
if(left.name !== right.name) return left.name < right.name
return left.gxs_id < right.gxs_id
},
function(left, right)
{
if(searchText.length > 0)
{
var mtc = searchText.text.toLowerCase()
var lfn = left.name.toLowerCase()
var rgn = right.name.toLowerCase()
var lfml = lfn.indexOf(mtc)
var rgml = rgn.indexOf(mtc)
if ( lfml !== rgml )
{
lfml = lfml >= 0 ? lfml : Number.MAX_VALUE
rgml = rgml >= 0 ? rgml : Number.MAX_VALUE
return lfml < rgml
}
}
return lessThan[0](left, right)
}
]
property int sortOrder: contactsView.searching ? 1 : 0
onSortOrderChanged: resetSorting()
property bool isSorting: false
function insertPosition(lessThan, item)
{
var lower = 0
var upper = items.count
while (lower < upper)
{
var middle = Math.floor(lower + (upper - lower) / 2)
var result = lessThan(item.model, items.get(middle).model);
if (result) upper = middle
else lower = middle + 1
}
return lower
}
function resetSorting() { items.setGroups(0, items.count, "unsorted") }
function sort()
{
while (unsortedItems.count > 0)
{
var item = unsortedItems.get(0)
var index = insertPosition(lessThan[visualModel.sortOrder],
item)
item.groups = ["items"]
items.move(item.itemsIndex, index)
}
}
items.includeByDefault: false
groups:
[
DelegateModelGroup
{
id: unsortedItems
name: "unsorted"
includeByDefault: true
onChanged: visualModel.sort()
}
]
delegate: GxsIdentityDelegate {}
}
ListView
{
id: locationsListView
width: parent.width
height: 300
model: locationsModel.model
delegate: Item
height: contactsView.searching ?
parent.height - searchBox.height : parent.height
model: visualModel
anchors.top: contactsView.searching ? searchBox.bottom : parent.top
}
Rectangle
{
id: searchBox
visible: contactsView.searching
height: searchText.height
width: searchText.width
anchors.right: parent.right
anchors.top: parent.top
Image
{
height: 40
width: parent.width
id: searchIcon
height: searchText.height - 4
width: searchText.height - 4
anchors.verticalCenter: parent.verticalCenter
source: "qrc:/qml/icons/edit-find.png"
}
MouseArea
{
anchors.fill: parent
onClicked:
{
console.log("Contacts view onclicked:", model.name,
model.gxs_id)
if(model.own) contactsView.own_gxs_id = model.gxs_id
else
{
var jsonData = { "own_gxs_hex": contactsView.own_gxs_id,
"remote_gxs_hex": model.gxs_id }
rsApi.request("/chat/initiate_distant_chat",
JSON.stringify(jsonData),
contactsView.startChatCallback)
}
}
Rectangle
{
id: colorHash
height: parent.height - 4
width: height
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 2
color: "white"
property int childHeight : height/2
Rectangle
{
color: '#' + model.gxs_id.substring(1, 9)
height: parent.childHeight
width: height
anchors.top: parent.top
anchors.left: parent.left
}
Rectangle
{
color: '#' + model.gxs_id.substring(9, 17)
height: parent.childHeight
width: height
anchors.top: parent.top
anchors.right: parent.right
}
Rectangle
{
color: '#' + model.gxs_id.substring(17, 25)
height: parent.childHeight
width: height
anchors.bottom: parent.bottom
anchors.left: parent.left
}
Rectangle
{
color: '#' + model.gxs_id.slice(-8)
height: parent.childHeight
width: height
anchors.bottom: parent.bottom
anchors.right: parent.right
}
MouseArea
{
anchors.fill: parent
onPressAndHold:
{
fingerPrintDialog.nick = model.name
fingerPrintDialog.gxs_id = model.gxs_id
fingerPrintDialog.visible = true
}
}
}
Text
{
id: nickText
color: model.own ? "blue" : "black"
text: model.name
anchors.left: colorHash.right
anchors.leftMargin: 5
anchors.verticalCenter: parent.verticalCenter
}
}
TextField
{
id: searchText
anchors.left: searchIcon.right
anchors.verticalCenter: parent.verticalCenter
onTextChanged: visualModel.resetSorting()
}
}
@ -179,11 +243,16 @@ Item
id: refreshTimer
interval: 5000
repeat: true
onTriggered: if(contactsView.visible) contactsView.refreshData()
triggeredOnStart: true
onTriggered:
if(contactsView.visible)
{
contactsView.refreshUnread()
contactsView.refreshData()
}
Component.onCompleted: start()
}
Dialog
{
id: createIdentityDialog
@ -207,14 +276,17 @@ Item
}
}
Dialog
Popup
{
id: fingerPrintDialog
visible: false
property string nick
property string gxs_id
title: nick + " fingerprint:"
standardButtons: StandardButton.NoButton
width: fingerPrintText.contentWidth + 20
height: fingerPrintText.contentHeight + 20
x: parent.x + parent.width/2 - width/2
y: parent.y + parent.height/2 - height/2
Text
{
id: fingerPrintText

View File

@ -0,0 +1,139 @@
/*
* RetroShare Android QML App
* Copyright (C) 2017 Gioacchino Mazzurco <gio@eigenlab.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import QtQuick 2.0
Item
{
height: 40
width: parent.width
MouseArea
{
anchors.fill: parent
onClicked:
{
console.log("Contacts view onclicked:", model.name,
model.gxs_id)
contactsView.searching = false
if(model.own) contactsView.own_gxs_id = model.gxs_id
else
{
var jsonData = { "own_gxs_hex": contactsView.own_gxs_id,
"remote_gxs_hex": model.gxs_id }
rsApi.request("/chat/initiate_distant_chat",
JSON.stringify(jsonData),
contactsView.startChatCallback)
}
}
Rectangle
{
id: colorHash
height: parent.height - 4
width: height
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 2
color: "white"
property int childHeight : height/2
Image
{
source: "qrc:/qml/icons/edit-image-face-detect.png"
anchors.fill: parent
}
Rectangle
{
color: '#' + model.gxs_id.substring(1, 9)
height: parent.childHeight
width: height
anchors.top: parent.top
anchors.left: parent.left
}
Rectangle
{
color: '#' + model.gxs_id.substring(9, 17)
height: parent.childHeight
width: height
anchors.top: parent.top
anchors.right: parent.right
}
Rectangle
{
color: '#' + model.gxs_id.substring(17, 25)
height: parent.childHeight
width: height
anchors.bottom: parent.bottom
anchors.left: parent.left
}
Rectangle
{
color: '#' + model.gxs_id.slice(-8)
height: parent.childHeight
width: height
anchors.bottom: parent.bottom
anchors.right: parent.right
}
MouseArea
{
anchors.fill: parent
onPressAndHold:
{
fingerPrintDialog.nick = model.name
fingerPrintDialog.gxs_id = model.gxs_id
fingerPrintDialog.visible = true
}
}
}
Text
{
id: nickText
color: model.own ? "blue" : "black"
text: model.name
anchors.left: colorHash.right
anchors.leftMargin: 5
anchors.verticalCenter: parent.verticalCenter
}
Rectangle
{
visible: contactsView.unreadMessages.hasOwnProperty(model.gxs_id)
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
color: "cornflowerblue"
antialiasing: true
border.color: "blue"
border.width: 1
radius: height/2
height: parent.height - 10
width: height
Text
{
color: "white"
font.bold: true
text: contactsView.unreadMessages.hasOwnProperty(model.gxs_id) ?
contactsView.unreadMessages[model.gxs_id] : ''
anchors.centerIn: parent
}
}
}
}

View File

@ -17,7 +17,7 @@
*/
import QtQuick 2.0
import QtQuick.Controls 1.4
import QtQuick.Controls 2.0
import org.retroshare.qml_components.LibresapiLocalClient 1.0
Item
@ -182,6 +182,7 @@ Item
id: attemptTimer
interval: 500
repeat: true
triggeredOnStart: true
onTriggered:
{
if(locationView.focus)

View File

@ -18,7 +18,7 @@
import QtQuick 2.0
import QtQuick.Layouts 1.3
import QtQuick.Controls 1.4
import QtQuick.Controls 2.0
import QtQml 2.2
import org.retroshare.qml_components.LibresapiLocalClient 1.0

View File

@ -17,7 +17,7 @@
*/
import QtQuick 2.0
import QtQuick.Controls 1.4
import QtQuick.Controls 2.0
import QtQuick.Dialogs 1.2
import "jsonpath.js" as JSONPath
@ -40,8 +40,10 @@ Item
function isOnline(pgpId)
{
var qr = "$.data[?(@.pgp_id=='"+pgpId+"')].locations[*].is_online"
var locArr = JSONPath.jsonPath(JSON.parse(jsonModel.json), qr)
return locArr.reduce(function(cur,acc){return cur || acc}, false)
var locOn = JSONPath.jsonPath(JSON.parse(jsonModel.json), qr)
if (Array.isArray(locOn))
return locOn.reduce(function(cur,acc){return cur || acc}, false)
return Boolean(locOn)
}
}
@ -65,7 +67,8 @@ Item
height: parent.height - 4
fillMode: Image.PreserveAspectFit
anchors.leftMargin: 6
anchors.left: parent.left
anchors.leftMargin: 3
anchors.verticalCenter: parent.verticalCenter
}
Text
@ -130,7 +133,7 @@ Item
id: bottomButton
text: "Add Trusted Node"
anchors.bottom: parent.bottom
onClicked: stackView.push({item:"qrc:/qml/AddTrustedNode.qml"})
onClicked: stackView.push("qrc:/qml/AddTrustedNode.qml")
width: parent.width
}
@ -138,6 +141,7 @@ Item
{
interval: 800
repeat: true
triggeredOnStart: true
onTriggered: if(trustedNodesView.visible) trustedNodesView.refreshData()
Component.onCompleted: start()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -17,7 +17,7 @@
*/
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls 2.0
import org.retroshare.qml_components.LibresapiLocalClient 1.0
ApplicationWindow
@ -28,8 +28,10 @@ ApplicationWindow
width: 400
height: 400
toolBar: ToolBar
header: ToolBar
{
id: toolBar
Image
{
id: rsIcon
@ -45,22 +47,48 @@ ApplicationWindow
anchors.left: rsIcon.right
anchors.leftMargin: 20
}
}
menuBar: MenuBar
{
Menu
MouseArea
{
MenuItem
height: parent.height
width: parent.height
anchors.right: parent.right
anchors.rightMargin: 2
anchors.verticalCenter: parent.verticalCenter
onClicked: menu.open()
Image
{
text: "Trusted Nodes"
onTriggered:
stackView.push({item:"qrc:/qml/TrustedNodesView.qml"})
source: "qrc:/qml/icons/application-menu.png"
height: parent.height - 10
width: parent.height - 10
anchors.centerIn: parent
}
MenuItem
Menu
{
text: "StackView State"
onTriggered: console.log(stackView.state, stackView.enabled)
id: menu
y: parent.y + parent.height
MenuItem
{
text: qsTr("Trusted Nodes")
//iconSource: "qrc:/qml/icons/document-share.png"
onTriggered:
stackView.push("qrc:/qml/TrustedNodesView.qml")
}
MenuItem
{
text: qsTr("Search Contacts")
onTriggered:
stackView.push(
"qrc:/qml/Contacts.qml", {'searching': true} )
}
MenuItem
{
text: "Terminate Core"
onTriggered: rsApi.request("/control/shutdown")
}
}
}
}
@ -102,6 +130,7 @@ ApplicationWindow
id: refreshTimer
interval: 800
repeat: true
triggeredOnStart: true
onTriggered: if(stackView.visible) stackView.checkCoreStatus()
Component.onCompleted: start()
}
@ -123,7 +152,7 @@ ApplicationWindow
{
console.log("StateChangeScript waiting_account_select")
stackView.clear()
stackView.push({item:"qrc:/qml/Locations.qml"})
stackView.push("qrc:/qml/Locations.qml")
}
}
},
@ -137,7 +166,7 @@ ApplicationWindow
{
console.log("StateChangeScript waiting_account_select")
stackView.clear()
stackView.push({item: "qrc:/qml/Contacts.qml"})
stackView.push("qrc:/qml/Contacts.qml")
}
}
},

View File

@ -12,6 +12,8 @@ RESOURCES += qml.qrc
# Additional import path used to resolve QML modules in Qt Creator's code model
#QML_IMPORT_PATH =
#QML2_IMPORT_PATH =
# Default rules for deployment.
include(deployment.pri)