Improve QML app Contacts view performances

Simplified sorting getting rid of complicated DelegateModel
Offload sorting work to another thread via WorkerScript
Get rid of polling and use token system instead
This commit is contained in:
Gioacchino Mazzurco 2017-04-03 21:51:03 +02:00
parent 7d9e89e3d2
commit 8e03fab8da
4 changed files with 166 additions and 154 deletions

View File

@ -18,5 +18,6 @@
<file>qml/icons/edit-image-face-detect.png</file>
<file>qml/icons/document-share.png</file>
<file>qml/icons/application-menu.png</file>
<file>qml/ContactSort.js</file>
</qresource>
</RCC>

View File

@ -0,0 +1,95 @@
/*
* 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/>.
*/
function strcmp(left, right)
{ return ( left < right ? -1 : ( left > right ? 1:0 ) ) }
var unreadMessages = {}
var contactsData = {}
function cntcmp(left, right, searchText)
{
if(typeof searchText !== 'undefined' && searchText.length > 0)
{
var mtc = searchText.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
}
}
var lfun = left.hasOwnProperty("unread_count") ? left.unread_count : 0
var rgun = right.hasOwnProperty("unread_count") ? right.unread_count : 0
if( lfun !== rgun ) return rgun - lfun
if(left.name !== right.name) return strcmp(left.name, right.name)
return strcmp(left.gxs_id, right.gxs_id)
}
function mergeContactsUnread()
{
var jsonData = contactsData.data
var dataLen = jsonData.length
for ( var i=0; i<dataLen; ++i)
{
var el = jsonData[i]
if(unreadMessages.hasOwnProperty(el.gxs_id))
el['unread_count'] = unreadMessages[el.gxs_id]
}
}
function parseUnread(responseStr)
{
var jsonData = JSON.parse(responseStr).data
var dataLen = jsonData.length
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
}
mergeContactsUnread()
}
function parseContacts(responseStr)
{
contactsData = JSON.parse(responseStr)
mergeContactsUnread()
}
WorkerScript.onMessage = function(message)
{
var sortFn = cntcmp
message.action = message.hasOwnProperty("action") ? message.action : "rSort"
if(message.action === "refreshContacts") parseContacts(message.response)
else if(message.action === "refreshUnread") parseUnread(message.response)
else if(message.action === "searchContact")
sortFn = function cmp(l,r) { return cntcmp(l,r, message.sexp) }
contactsData.data.sort(sortFn)
WorkerScript.sendMessage(contactsData)
}

View File

@ -28,58 +28,83 @@ Item
property string own_gxs_id: ""
property string own_nick: ""
property bool searching: false
property var unreadMessages: ({})
onSearchingChanged: !searching && contactsSortWorker.sendMessage({})
Component.onCompleted: refreshOwn()
Component.onCompleted: refreshAll()
onFocusChanged: focus && refreshAll()
function refreshData()
WorkerScript
{
function refreshCallback(par)
id: contactsSortWorker
source: "qrc:/qml/ContactSort.js"
onMessage: contactsListModel.json = JSON.stringify(messageObject)
}
function refreshAll()
{
refreshOwn()
refreshContacts()
refreshUnread()
}
function refreshContactsCallback(par)
{
console.log("contactsView.refreshContactsCB()", visible)
if (contactsListModel.model.count < 1)
contactsListModel.json = par.response
var token = JSON.parse(par.response).statetoken
mainWindow.registerToken(token, refreshContacts)
contactsSortWorker.sendMessage(
{'action': 'refreshContacts', 'response': par.response})
}
function refreshContacts()
{
console.log("contactsView.refreshContacts()", visible)
if(!visible) return
rsApi.request("/identity/*/", "", refreshContactsCallback)
}
function refreshOwnCallback(par)
{
console.log("contactsView.refreshOwnCallback(par)", visible)
var json = JSON.parse(par.response)
var token = json.statetoken
mainWindow.registerToken(token, refreshOwn)
if(json.data.length > 0)
{
gxsIdsModel.json = par.response
if(contactsView.own_gxs_id == "") refreshOwn()
contactsView.own_gxs_id = json.data[0].gxs_id
contactsView.own_nick = json.data[0].name
}
rsApi.request("/identity/*/", "", refreshCallback)
else createIdentityDialog.visible = true
}
function refreshOwn()
{
rsApi.request("/identity/own", "", function(par)
{
var json = JSON.parse(par.response)
if(json.data.length > 0)
{
contactsView.own_gxs_id = json.data[0].gxs_id
contactsView.own_nick = json.data[0].name
}
else createIdentityDialog.visible = true
})
console.log("contactsView.refreshOwn()", visible)
rsApi.request("/identity/own", "", refreshOwnCallback)
}
function refreshUnreadCallback(par)
{
console.log("contactsView.refreshUnreadCB()", visible)
var json = JSON.parse(par.response)
mainWindow.registerToken(json.statetoken, refreshUnread)
contactsSortWorker.sendMessage(
{'action': 'refreshUnread', 'response': par.response})
}
function refreshUnread()
{
console.log("contactsView.refreshUnread()", visible)
if(!visible) return
rsApi.request("/chat/unread_msgs", "", refreshUnreadCallback)
}
function startChatCallback(par)
{
var chId = JSON.parse(par.response).data.chat_id
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(...) */
@ -88,116 +113,21 @@ Item
return from_gxs < to_gxs ? from_gxs + to_gxs : to_gxs + from_gxs
}
onFocusChanged: focus && refreshData()
JSONListModel
{
id: gxsIdsModel
id: contactsListModel
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: contactsView.searching ?
parent.height - searchBox.height : parent.height
model: visualModel
model: contactsListModel.model
anchors.top: contactsView.searching ? searchBox.bottom : parent.top
delegate: GxsIdentityDelegate {}
}
Rectangle
@ -224,7 +154,9 @@ Item
id: searchText
anchors.left: searchIcon.right
anchors.verticalCenter: parent.verticalCenter
onTextChanged: visualModel.resetSorting()
onTextChanged:
contactsSortWorker.sendMessage(
{'action': 'searchContact', 'sexp': text})
}
}
@ -238,21 +170,6 @@ Item
text: "Open Chat as: " + contactsView.own_nick + " " + contactsView.own_gxs_id
}
Timer
{
id: refreshTimer
interval: 5000
repeat: true
triggeredOnStart: true
onTriggered:
if(contactsView.visible)
{
contactsView.refreshUnread()
contactsView.refreshData()
}
Component.onCompleted: start()
}
Dialog
{
id: createIdentityDialog

View File

@ -113,7 +113,7 @@ Item
}
Rectangle
{
visible: contactsView.unreadMessages.hasOwnProperty(model.gxs_id)
visible: model.unread_count > 0
anchors.right: parent.right
anchors.rightMargin: 10
@ -130,8 +130,7 @@ Item
{
color: "white"
font.bold: true
text: contactsView.unreadMessages.hasOwnProperty(model.gxs_id) ?
contactsView.unreadMessages[model.gxs_id] : ''
text: model.unread_count > 0 ? model.unread_count : ''
anchors.centerIn: parent
}
}