Upgrade search and message loading. More smart loading entries based on currently view.

Add a spinner while loading/searching.
This commit is contained in:
Julien Wadel 2021-09-16 19:34:40 +02:00
parent d6ca4b1ab8
commit 99b9a7753c
7 changed files with 350 additions and 310 deletions

View file

@ -47,7 +47,7 @@ if(WIN32)
endif()
elseif( APPLE )
if( NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9")
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.9" CACHE STRING "Minimum OS X deployment version")
endif()
endif()

View file

@ -773,7 +773,7 @@ void ChatRoomModel::initEntries(){
}
}
void ChatRoomModel::loadMoreEntries(){
int ChatRoomModel::loadMoreEntries(){
QList<std::shared_ptr<ChatEvent> > entries;
QList<EntrySorterHelper> prepareEntries;
// Get current event count for each type
@ -838,6 +838,7 @@ void ChatRoomModel::loadMoreEntries(){
emit layoutChanged();
updateLastUpdateTime();
}
return entries.size();
}
//-------------------------------------------------

View file

@ -218,7 +218,7 @@ public:
void compose ();
void resetMessageCount ();
Q_INVOKABLE void initEntries();
Q_INVOKABLE void loadMoreEntries();
Q_INVOKABLE int loadMoreEntries(); // return new entries count
void callEnded(std::shared_ptr<linphone::Call> call);
QDateTime mLastUpdateTime;

View file

@ -140,17 +140,14 @@ void ChatRoomProxyModel::compose (const QString& text) {
void ChatRoomProxyModel::loadMoreEntries () {
if(mChatRoomModel ) {
int count = rowCount();
int parentCount = sourceModel()->rowCount();
if (count == mMaxDisplayedEntries)
mMaxDisplayedEntries += EntriesChunkSize;
if (count + 10 >= parentCount) // Magic number : try to load more entries if near to max event count
mChatRoomModel->loadMoreEntries();
invalidateFilter();
count = rowCount() - count;
if (count > 0)
emit moreEntriesLoaded(count);
int currentRowCount = rowCount();
int newEntries = 0;
do{
newEntries = mChatRoomModel->loadMoreEntries();
invalidate();
}while( newEntries>0 && currentRowCount == rowCount());
currentRowCount = rowCount() - currentRowCount + 1;
emit moreEntriesLoaded(currentRowCount);
}
}
@ -262,7 +259,6 @@ QString ChatRoomProxyModel::getCachedText() const{
// -----------------------------------------------------------------------------
void ChatRoomProxyModel::reload (ChatRoomModel *chatRoomModel) {
mMaxDisplayedEntries = EntriesChunkSize;
if (mChatRoomModel) {
ChatRoomModel *ChatRoomModel = mChatRoomModel.get();
@ -294,8 +290,13 @@ void ChatRoomProxyModel::resetMessageCount(){
void ChatRoomProxyModel::setFilterText(const QString& text){
if( mFilterText != text){
mFilterText = text;
invalidate();
emit filterTextChanged();
int currentRowCount = rowCount();
int newEntries = 0;
do{
newEntries = mChatRoomModel->loadMoreEntries();
invalidate();
emit filterTextChanged();
}while( newEntries>0 && currentRowCount == rowCount());
}
}
@ -336,7 +337,6 @@ void ChatRoomProxyModel::handleIsRemoteComposingChanged () {
}
void ChatRoomProxyModel::handleMessageReceived (const shared_ptr<linphone::ChatMessage> &) {
mMaxDisplayedEntries++;
QWindow *window = getParentWindow(this);
if (window && window->isActive())
@ -344,5 +344,4 @@ void ChatRoomProxyModel::handleMessageReceived (const shared_ptr<linphone::ChatM
}
void ChatRoomProxyModel::handleMessageSent (const shared_ptr<linphone::ChatMessage> &) {
mMaxDisplayedEntries++;
}

View file

@ -34,14 +34,6 @@ function initView () {
chat.bindToEnd = true
}
function loadMoreEntries () {
if (chat.atYBeginning && !chat.tryToLoadMoreEntries) {
chat.tryToLoadMoreEntries = true
chat.positionViewAtBeginning()
container.proxyModel.loadMoreEntries()
}
}
function getComponentFromEntry (chatEntry) {
if (chatEntry.fileContentModel && chatEntry.fileContentModel.name) {
return 'FileMessage.qml'
@ -77,7 +69,6 @@ function handleFilesDropped (files) {
function handleMoreEntriesLoaded (n) {
chat.positionViewAtIndex(n - 1, QtQuick.ListView.Beginning)
chat.tryToLoadMoreEntries = false
}
function handleMovementEnded () {

View file

@ -11,292 +11,330 @@ import 'Chat.js' as Logic
// =============================================================================
Rectangle {
id: container
property alias proxyModel: chat.model // ChatRoomProxyModel
// ---------------------------------------------------------------------------
signal messageToSend (string text)
// ---------------------------------------------------------------------------
color: ChatStyle.color
ColumnLayout {
anchors.fill: parent
spacing: 0
ScrollableListView {
id: chat
// -----------------------------------------------------------------------
property bool bindToEnd: false
property bool tryToLoadMoreEntries: true
//property var sipAddressObserver: SipAddressesModel.getSipAddressObserver(proxyModel.fullPeerAddress, proxyModel.fullLocalAddress)
// -----------------------------------------------------------------------
Layout.fillHeight: true
Layout.fillWidth: true
highlightFollowsCurrentItem: false
section {
criteria: ViewSection.FullString
delegate: sectionHeading
property: '$sectionDate'
}
// -----------------------------------------------------------------------
Component.onCompleted: Logic.initView()
onContentYChanged: Logic.loadMoreEntries()
onMovementEnded: Logic.handleMovementEnded()
onMovementStarted: Logic.handleMovementStarted()
// -----------------------------------------------------------------------
Connections {
target: proxyModel
// When the view is changed (for example `Calls` -> `Messages`),
// the position is set at end and it can be possible to load
// more entries.
onEntryTypeFilterChanged: Logic.initView()
onMoreEntriesLoaded: Logic.handleMoreEntriesLoaded(n)
}
// -----------------------------------------------------------------------
// Heading.
// -----------------------------------------------------------------------
Component {
id: sectionHeading
Item {
implicitHeight: container.height + ChatStyle.sectionHeading.bottomMargin
width: parent.width
Borders {
id: container
borderColor: ChatStyle.sectionHeading.border.color
bottomWidth: ChatStyle.sectionHeading.border.width
implicitHeight: text.contentHeight +
ChatStyle.sectionHeading.padding * 2 +
ChatStyle.sectionHeading.border.width * 2
topWidth: ChatStyle.sectionHeading.border.width
width: parent.width
Text {
id: text
anchors.fill: parent
color: ChatStyle.sectionHeading.text.color
font {
bold: true
pointSize: ChatStyle.sectionHeading.text.pointSize
}
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
// Cast section to integer because Qt converts the
// sectionDate in string!!!
text: new Date(section).toLocaleDateString(
Qt.locale(App.locale)
)
}
}
}
}
// -----------------------------------------------------------------------
// Message/Event renderer.
// -----------------------------------------------------------------------
delegate: Rectangle {
id: entry
property bool isNotice : $chatEntry.type === ChatRoomModel.NoticeEntry
property bool isCall : $chatEntry.type === ChatRoomModel.CallEntry
property bool isMessage : $chatEntry.type === ChatRoomModel.MessageEntry
function isHoverEntry () {
return mouseArea.containsMouse
}
function removeEntry () {
proxyModel.removeRow(index)
}
anchors {
left: parent ? parent.left : undefined
leftMargin: isNotice?0:ChatStyle.entry.leftMargin
right: parent ? parent.right : undefined
rightMargin: isNotice?0:ChatStyle.entry.deleteIconSize +
ChatStyle.entry.message.extraContent.spacing +
ChatStyle.entry.message.extraContent.rightMargin +
ChatStyle.entry.message.extraContent.leftMargin +
ChatStyle.entry.message.outgoing.areaSize
}
color: ChatStyle.color
implicitHeight: layout.height + ChatStyle.entry.bottomMargin
// ---------------------------------------------------------------------
MouseArea {
id: mouseArea
cursorShape: Qt.ArrowCursor
hoverEnabled: true
implicitHeight: layout.height
width: parent.width + parent.anchors.rightMargin
acceptedButtons: Qt.NoButton
ColumnLayout{
id: layout
spacing: 0
width: entry.width
Text{
id:authorName
Layout.leftMargin: timeDisplay.width + 10
Layout.fillWidth: true
text : $chatEntry.fromDisplayName ? $chatEntry.fromDisplayName : ''
property var previousItem : {
if(index >0)
return proxyModel.getAt(index-1)
else
return null
}
id: container
property alias proxyModel: chat.model // ChatRoomProxyModel
property alias tryingToLoadMoreEntries : chat.tryToLoadMoreEntries
// ---------------------------------------------------------------------------
signal messageToSend (string text)
// ---------------------------------------------------------------------------
color: ChatStyle.color
ColumnLayout {
anchors.fill: parent
spacing: 0
ScrollableListView {
id: chat
// -----------------------------------------------------------------------
property bool bindToEnd: false
property bool tryToLoadMoreEntries: true
//property var sipAddressObserver: SipAddressesModel.getSipAddressObserver(proxyModel.fullPeerAddress, proxyModel.fullLocalAddress)
// -----------------------------------------------------------------------
Layout.fillHeight: true
Layout.fillWidth: true
highlightFollowsCurrentItem: false
section {
criteria: ViewSection.FullString
delegate: sectionHeading
property: '$sectionDate'
}
Timer {
id: loadMoreEntriesDelayer
interval: 1
repeat: false
running: false
onTriggered: {
chat.positionViewAtBeginning()
container.proxyModel.loadMoreEntries()
}
}
Timer {
// Delay each search by 100ms
id: endOfLoadMoreEntriesDelayer
interval: 100
repeat: false
running: false
onTriggered: {
if(chat.atYBeginning){// We are still at the beginning. Try to continue searching
loadMoreEntriesDelayer.start()
}else// We are not at the begining. New search can be done by moving to the top.
chat.tryToLoadMoreEntries = false
}
}
// -----------------------------------------------------------------------
Component.onCompleted: Logic.initView()
onContentYChanged: {
if (chat.atYBeginning && !chat.tryToLoadMoreEntries) {
chat.tryToLoadMoreEntries = true// Show busy indicator
loadMoreEntriesDelayer.start()// Let GUI time to the busy indicator to be shown
}
}
onMovementEnded: Logic.handleMovementEnded()
onMovementStarted: Logic.handleMovementStarted()
// -----------------------------------------------------------------------
Connections {
target: proxyModel
// When the view is changed (for example `Calls` -> `Messages`),
// the position is set at end and it can be possible to load
// more entries.
onEntryTypeFilterChanged: Logic.initView()
onMoreEntriesLoaded: {
Logic.handleMoreEntriesLoaded(n)
if(n>1)// New entries : delay the end
endOfLoadMoreEntriesDelayer.start()
else// No new entries, we can stop without waiting
chat.tryToLoadMoreEntries = false
}
}
// -----------------------------------------------------------------------
// Heading.
// -----------------------------------------------------------------------
Component {
id: sectionHeading
Item {
implicitHeight: container.height + ChatStyle.sectionHeading.bottomMargin
width: parent.width
color: ChatStyle.entry.event.text.color
font.pointSize: ChatStyle.entry.event.text.pointSize
visible: isMessage
&& $chatEntry != undefined
&& !$chatEntry.isOutgoing // Only outgoing
&& (!previousItem //No previous entry
|| previousItem.type != ChatRoomModel.MessageEntry // Previous entry is a message
|| previousItem.fromSipAddress != $chatEntry.fromSipAddress // Different user
|| (new Date(previousItem.timestamp)).setHours(0, 0, 0, 0) != (new Date($chatEntry.timestamp)).setHours(0, 0, 0, 0) // Same day == section
)
Borders {
id: container
borderColor: ChatStyle.sectionHeading.border.color
bottomWidth: ChatStyle.sectionHeading.border.width
implicitHeight: text.contentHeight +
ChatStyle.sectionHeading.padding * 2 +
ChatStyle.sectionHeading.border.width * 2
topWidth: ChatStyle.sectionHeading.border.width
width: parent.width
Text {
id: text
anchors.fill: parent
color: ChatStyle.sectionHeading.text.color
font {
bold: true
pointSize: ChatStyle.sectionHeading.text.pointSize
}
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
// Cast section to integer because Qt converts the
// sectionDate in string!!!
text: new Date(section).toLocaleDateString(
Qt.locale(App.locale)
)
}
}
}
RowLayout {
spacing: 0
width: entry.width
// Display time.
Text {
id:timeDisplay
Layout.alignment: Qt.AlignTop
Layout.preferredHeight: ChatStyle.entry.lineHeight
Layout.preferredWidth: ChatStyle.entry.time.width
color: ChatStyle.entry.event.text.color
font.pointSize: ChatStyle.entry.time.pointSize
text: $chatEntry.timestamp.toLocaleString(
Qt.locale(App.locale),
'hh:mm'
)
verticalAlignment: Text.AlignVCenter
TooltipArea {
text: $chatEntry.timestamp.toLocaleString(Qt.locale(App.locale))
}
visible:!isNotice
}
// -----------------------------------------------------------------------
// Message/Event renderer.
// -----------------------------------------------------------------------
delegate: Rectangle {
id: entry
property bool isNotice : $chatEntry.type === ChatRoomModel.NoticeEntry
property bool isCall : $chatEntry.type === ChatRoomModel.CallEntry
property bool isMessage : $chatEntry.type === ChatRoomModel.MessageEntry
function isHoverEntry () {
return mouseArea.containsMouse
}
// Display content.
Loader {
Layout.fillWidth: true
source: Logic.getComponentFromEntry($chatEntry)
function removeEntry () {
proxyModel.removeRow(index)
}
}
}
}
}
footer: Text {
property var composers : container.proxyModel.composers
color: ChatStyle.composingText.color
font.pointSize: ChatStyle.composingText.pointSize
height: visible ? undefined : 0
leftPadding: ChatStyle.composingText.leftPadding
visible: composers.length > 0 && SettingsModel.chatEnabled
wrapMode: Text.Wrap
//: '%1 is typing...' indicate that someone is composing in chat
text:(composers.length==0?'': qsTr('chatTyping','',composers.length).arg(container.proxyModel.getDisplayNameComposers()))
anchors {
left: parent ? parent.left : undefined
leftMargin: isNotice?0:ChatStyle.entry.leftMargin
right: parent ? parent.right : undefined
rightMargin: isNotice?0:ChatStyle.entry.deleteIconSize +
ChatStyle.entry.message.extraContent.spacing +
ChatStyle.entry.message.extraContent.rightMargin +
ChatStyle.entry.message.extraContent.leftMargin +
ChatStyle.entry.message.outgoing.areaSize
}
}
// -------------------------------------------------------------------------
// Send area.
// -------------------------------------------------------------------------
Borders {
Layout.fillWidth: true
Layout.preferredHeight: textArea.height
borderColor: ChatStyle.sendArea.border.color
topWidth: ChatStyle.sendArea.border.width
visible: SettingsModel.chatEnabled && proxyModel.chatRoomModel && !proxyModel.chatRoomModel.hasBeenLeft
DroppableTextArea {
id: textArea
enabled:proxyModel && proxyModel.chatRoomModel ? !proxyModel.chatRoomModel.hasBeenLeft:false
isEphemeral : proxyModel && proxyModel.chatRoomModel ? proxyModel.chatRoomModel.ephemeralEnabled:false
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height:ChatStyle.sendArea.height + ChatStyle.sendArea.border.width
minimumHeight:ChatStyle.sendArea.height + ChatStyle.sendArea.border.width
maximumHeight:container.height/2
dropEnabled: SettingsModel.fileTransferUrl.length > 0
dropDisabledReason: qsTr('noFileTransferUrl')
placeholderText: qsTr('newMessagePlaceholder')
onDropped: Logic.handleFilesDropped(files)
onTextChanged: Logic.handleTextChanged(text)
onValidText: {
textArea.text = ''
chat.bindToEnd = true
if(proxyModel.chatRoomModel)
proxyModel.sendMessage(text)
else{
console.log("Peer : " +proxyModel.peerAddress+ "/"+chat.model.peerAddress)
proxyModel.chatRoomModel = CallsListModel.createChat(proxyModel.peerAddress)
proxyModel.sendMessage(text)
color: ChatStyle.color
implicitHeight: layout.height + ChatStyle.entry.bottomMargin
// ---------------------------------------------------------------------
MouseArea {
id: mouseArea
cursorShape: Qt.ArrowCursor
hoverEnabled: true
implicitHeight: layout.height
width: parent.width + parent.anchors.rightMargin
acceptedButtons: Qt.NoButton
ColumnLayout{
id: layout
spacing: 0
width: entry.width
Text{
id:authorName
Layout.leftMargin: timeDisplay.width + 10
Layout.fillWidth: true
text : $chatEntry.fromDisplayName ? $chatEntry.fromDisplayName : ''
property var previousItem : {
if(index >0)
return proxyModel.getAt(index-1)
else
return null
}
color: ChatStyle.entry.event.text.color
font.pointSize: ChatStyle.entry.event.text.pointSize
visible: isMessage
&& $chatEntry != undefined
&& !$chatEntry.isOutgoing // Only outgoing
&& (!previousItem //No previous entry
|| previousItem.type != ChatRoomModel.MessageEntry // Previous entry is a message
|| previousItem.fromSipAddress != $chatEntry.fromSipAddress // Different user
|| (new Date(previousItem.timestamp)).setHours(0, 0, 0, 0) != (new Date($chatEntry.timestamp)).setHours(0, 0, 0, 0) // Same day == section
)
}
RowLayout {
spacing: 0
width: entry.width
// Display time.
Text {
id:timeDisplay
Layout.alignment: Qt.AlignTop
Layout.preferredHeight: ChatStyle.entry.lineHeight
Layout.preferredWidth: ChatStyle.entry.time.width
color: ChatStyle.entry.event.text.color
font.pointSize: ChatStyle.entry.time.pointSize
text: $chatEntry.timestamp.toLocaleString(
Qt.locale(App.locale),
'hh:mm'
)
verticalAlignment: Text.AlignVCenter
TooltipArea {
text: $chatEntry.timestamp.toLocaleString(Qt.locale(App.locale))
}
visible:!isNotice
}
// Display content.
Loader {
Layout.fillWidth: true
source: Logic.getComponentFromEntry($chatEntry)
}
}
}
}
}
footer: Text {
property var composers : container.proxyModel.composers
color: ChatStyle.composingText.color
font.pointSize: ChatStyle.composingText.pointSize
height: visible ? undefined : 0
leftPadding: ChatStyle.composingText.leftPadding
visible: composers.length > 0 && SettingsModel.chatEnabled
wrapMode: Text.Wrap
//: '%1 is typing...' indicate that someone is composing in chat
text:(composers.length==0?'': qsTr('chatTyping','',composers.length).arg(container.proxyModel.getDisplayNameComposers()))
}
}
Component.onCompleted: {text = proxyModel.cachedText; cursorPosition=text.length}
Rectangle{
anchors.fill:parent
color:'white'
opacity: 0.5
visible:!textArea.enabled
// -------------------------------------------------------------------------
// Send area.
// -------------------------------------------------------------------------
Borders {
Layout.fillWidth: true
Layout.preferredHeight: textArea.height
borderColor: ChatStyle.sendArea.border.color
topWidth: ChatStyle.sendArea.border.width
visible: SettingsModel.chatEnabled && proxyModel.chatRoomModel && !proxyModel.chatRoomModel.hasBeenLeft
DroppableTextArea {
id: textArea
enabled:proxyModel && proxyModel.chatRoomModel ? !proxyModel.chatRoomModel.hasBeenLeft:false
isEphemeral : proxyModel && proxyModel.chatRoomModel ? proxyModel.chatRoomModel.ephemeralEnabled:false
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height:ChatStyle.sendArea.height + ChatStyle.sendArea.border.width
minimumHeight:ChatStyle.sendArea.height + ChatStyle.sendArea.border.width
maximumHeight:container.height/2
dropEnabled: SettingsModel.fileTransferUrl.length > 0
dropDisabledReason: qsTr('noFileTransferUrl')
placeholderText: qsTr('newMessagePlaceholder')
onDropped: Logic.handleFilesDropped(files)
onTextChanged: Logic.handleTextChanged(text)
onValidText: {
textArea.text = ''
chat.bindToEnd = true
if(proxyModel.chatRoomModel)
proxyModel.sendMessage(text)
else{
console.log("Peer : " +proxyModel.peerAddress+ "/"+chat.model.peerAddress)
proxyModel.chatRoomModel = CallsListModel.createChat(proxyModel.peerAddress)
proxyModel.sendMessage(text)
}
}
Component.onCompleted: {text = proxyModel.cachedText; cursorPosition=text.length}
Rectangle{
anchors.fill:parent
color:'white'
opacity: 0.5
visible:!textArea.enabled
}
}
}
}
}
}
// ---------------------------------------------------------------------------
// Scroll at end if necessary.
// ---------------------------------------------------------------------------
Timer {
interval: 100
repeat: true
running: true
onTriggered: chat.bindToEnd && chat.positionViewAtEnd()
}
}
// ---------------------------------------------------------------------------
// Scroll at end if necessary.
// ---------------------------------------------------------------------------
Timer {
interval: 100
repeat: true
running: true
onTriggered: chat.bindToEnd && chat.positionViewAtEnd()
}
}

View file

@ -414,6 +414,17 @@ ColumnLayout {
onClicked: Logic.updateChatFilter(button)
}
BusyIndicator{
id: chatLoading
width: 20
height: 20
anchors.left: filterButtons.right
anchors.leftMargin: 50
anchors.verticalCenter: parent.verticalCenter
//anchors.horizontalCenter: parent.horizontalCenter
visible: chatArea.tryingToLoadMoreEntries
}
// -------------------------------------------------------------------------
// Search.
// -------------------------------------------------------------------------
@ -422,9 +433,9 @@ ColumnLayout {
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.left : filterButtons.right
anchors.left : chatLoading.right
anchors.rightMargin: 10
anchors.leftMargin: 80
anchors.leftMargin: 50
anchors.topMargin: 10
anchors.bottomMargin: 10