Warning, /network/neochat/src/qml/TimelineView.qml is written in an unsupported language. File is not indexed.
0001 // SPDX-FileCopyrightText: 2020 Carl Schwan <carl@carlschwan.eu> 0002 // SPDX-License-Identifier: GPL-2.0-or-later 0003 0004 import QtQuick 0005 import QtQuick.Controls as QQC2 0006 import QtQuick.Layouts 0007 import Qt.labs.qmlmodels 0008 import QtQuick.Window 0009 0010 import org.kde.kirigamiaddons.components as KirigamiComponents 0011 import org.kde.kirigami as Kirigami 0012 import org.kde.kitemmodels 0013 0014 import org.kde.neochat 0015 import org.kde.neochat.config 0016 0017 QQC2.ScrollView { 0018 id: root 0019 required property NeoChatRoom currentRoom 0020 required property NeoChatConnection connection 0021 onCurrentRoomChanged: { 0022 roomChanging = true; 0023 roomChangingTimer.restart() 0024 applicationWindow().hoverLinkIndicator.text = ""; 0025 messageListView.positionViewAtBeginning(); 0026 hasScrolledUpBefore = false; 0027 } 0028 property bool roomChanging: false 0029 0030 /** 0031 * @brief The TimelineModel to use. 0032 * 0033 * Required so that new events can be requested when the end of the current 0034 * local timeline is reached. 0035 */ 0036 required property TimelineModel timelineModel 0037 0038 /** 0039 * @brief The MessageFilterModel to use. 0040 * 0041 * This model has the filtered list of events that should be shown in the timeline. 0042 */ 0043 required property MessageFilterModel messageFilterModel 0044 0045 /** 0046 * @brief The ActionsHandler object to use. 0047 * 0048 * This is expected to have the correct room set otherwise messages will be sent 0049 * to the wrong room. 0050 */ 0051 required property ActionsHandler actionsHandler 0052 0053 readonly property bool atYEnd: messageListView.atYEnd 0054 0055 property alias interactive: messageListView.interactive 0056 0057 /// Used to determine if scrolling to the bottom should mark the message as unread 0058 property bool hasScrolledUpBefore: false; 0059 0060 signal focusChatBar() 0061 0062 QQC2.ScrollBar.vertical.interactive: false 0063 0064 ListView { 0065 id: messageListView 0066 // So that delegates can access the current room properly. 0067 readonly property NeoChatRoom currentRoom: root.currentRoom 0068 // So that delegates can access the actionsHandler properly. 0069 readonly property ActionsHandler actionsHandler: root.actionsHandler 0070 0071 readonly property int largestVisibleIndex: count > 0 ? indexAt(contentX + (width / 2), contentY + height - 1) : -1 0072 readonly property var sectionBannerItem: contentHeight >= height ? itemAtIndex(sectionBannerIndex()) : undefined 0073 0074 // Spacing needs to be zero or the top sectionLabel overlay will be disrupted. 0075 // This is because itemAt returns null in the spaces. 0076 // All spacing should be handled by the delegates themselves 0077 spacing: 0 0078 verticalLayoutDirection: ListView.BottomToTop 0079 clip: true 0080 interactive: Kirigami.Settings.isMobile 0081 bottomMargin: Kirigami.Units.largeSpacing + Math.round(Kirigami.Theme.defaultFont.pointSize * 2) 0082 0083 model: root.messageFilterModel 0084 0085 Timer { 0086 interval: 1000 0087 running: messageListView.atYBeginning 0088 triggeredOnStart: true 0089 onTriggered: { 0090 if (messageListView.atYBeginning && root.timelineModel.messageEventModel.canFetchMore(root.timelineModel.index(0, 0))) { 0091 root.timelineModel.messageEventModel.fetchMore(root.timelineModel.index(0, 0)); 0092 } 0093 } 0094 repeat: true 0095 } 0096 0097 // HACK: The view should do this automatically but doesn't. 0098 onAtYBeginningChanged: if (atYBeginning && root.timelineModel.messageEventModel.canFetchMore(root.timelineModel.index(0, 0))) { 0099 root.timelineModel.messageEventModel.fetchMore(root.timelineModel.index(0, 0)); 0100 } 0101 0102 Timer { 0103 id: roomChangingTimer 0104 interval: 1000 0105 onTriggered: { 0106 root.roomChanging = false 0107 markReadIfVisibleTimer.reset() 0108 } 0109 } 0110 onAtYEndChanged: if (!root.roomChanging) { 0111 if (atYEnd && root.hasScrolledUpBefore) { 0112 if (QQC2.ApplicationWindow.window && (QQC2.ApplicationWindow.window.visibility !== QQC2.ApplicationWindow.Hidden)) { 0113 root.currentRoom.markAllMessagesAsRead(); 0114 } 0115 root.hasScrolledUpBefore = false; 0116 } else if (!atYEnd) { 0117 root.hasScrolledUpBefore = true; 0118 } 0119 } 0120 0121 // Not rendered because the sections are part of the MessageDelegate.qml, this is only so that items have the section property available for use by sectionBanner. 0122 // This is due to the fact that the ListView verticalLayout is BottomToTop. 0123 // This also flips the sections which would appear at the bottom but for a timeline they still need to be at the top (bottom from the qml perspective). 0124 // There is currently no option to put section headings at the bottom in qml. 0125 section.property: "section" 0126 0127 function sectionBannerIndex() { 0128 let center = messageListView.x + messageListView.width / 2; 0129 let yStart = messageListView.y + messageListView.contentY; 0130 let index = -1; 0131 let i = 0; 0132 while (index === -1 && i < 100) { 0133 index = messageListView.indexAt(center, yStart + i); 0134 i++; 0135 } 0136 return index; 0137 } 0138 0139 footer: SectionDelegate { 0140 id: sectionBanner 0141 0142 anchors.left: parent.left 0143 anchors.leftMargin: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.contentItem.parent.x : 0 0144 anchors.right: parent.right 0145 0146 maxWidth: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.contentItem.width : 0 0147 z: 3 0148 visible: !!messageListView.sectionBannerItem && messageListView.sectionBannerItem.ListView.section !== "" && !Config.blur 0149 labelText: messageListView.sectionBannerItem ? messageListView.sectionBannerItem.ListView.section : "" 0150 colorSet: Config.compactLayout ? Kirigami.Theme.View : Kirigami.Theme.Window 0151 } 0152 footerPositioning: ListView.OverlayHeader 0153 0154 delegate: EventDelegate { 0155 room: root.currentRoom 0156 connection: root.connection 0157 } 0158 0159 KirigamiComponents.FloatingButton { 0160 id: goReadMarkerFab 0161 0162 anchors.right: parent.right 0163 anchors.top: parent.top 0164 anchors.topMargin: Kirigami.Units.largeSpacing 0165 anchors.rightMargin: Kirigami.Units.largeSpacing 0166 implicitWidth: Kirigami.Units.gridUnit * 2 0167 implicitHeight: Kirigami.Units.gridUnit * 2 0168 0169 z: 2 0170 visible: root.currentRoom && root.currentRoom.hasUnreadMessages && root.currentRoom.readMarkerLoaded 0171 action: Kirigami.Action { 0172 onTriggered: { 0173 if (!Kirigami.Settings.isMobile) { 0174 root.focusChatBar(); 0175 } 0176 messageListView.goToEvent(root.currentRoom.readMarkerEventId) 0177 } 0178 icon.name: "go-up" 0179 shortcut: "Shift+PgUp" 0180 } 0181 0182 QQC2.ToolTip { 0183 text: i18n("Jump to first unread message") 0184 } 0185 } 0186 KirigamiComponents.FloatingButton { 0187 id: goMarkAsReadFab 0188 anchors.right: parent.right 0189 anchors.bottom: parent.bottom 0190 anchors.bottomMargin: Kirigami.Units.largeSpacing 0191 anchors.rightMargin: Kirigami.Units.largeSpacing 0192 implicitWidth: Kirigami.Units.gridUnit * 2 0193 implicitHeight: Kirigami.Units.gridUnit * 2 0194 0195 z: 2 0196 visible: !messageListView.atYEnd 0197 action: Kirigami.Action { 0198 onTriggered: { 0199 messageListView.goToLastMessage(); 0200 root.currentRoom.markAllMessagesAsRead(); 0201 } 0202 icon.name: "go-down" 0203 } 0204 0205 QQC2.ToolTip { 0206 text: i18n("Jump to latest message") 0207 } 0208 } 0209 0210 Component.onCompleted: { 0211 positionViewAtBeginning(); 0212 } 0213 0214 DropArea { 0215 id: dropAreaFile 0216 anchors.fill: parent 0217 onDropped: root.currentRoom.mainCache.attachmentPath = drop.urls[0] 0218 enabled: !Controller.isFlatpak 0219 } 0220 0221 QQC2.Pane { 0222 visible: dropAreaFile.containsDrag 0223 anchors { 0224 fill: parent 0225 margins: Kirigami.Units.gridUnit 0226 } 0227 0228 Kirigami.PlaceholderMessage { 0229 anchors.centerIn: parent 0230 width: parent.width - (Kirigami.Units.largeSpacing * 4) 0231 text: i18n("Drag items here to share them") 0232 } 0233 } 0234 0235 TypingPane { 0236 id: typingPane 0237 visible: root.currentRoom && root.currentRoom.usersTyping.length > 0 0238 labelText: visible ? i18ncp( 0239 "Message displayed when some users are typing", "%2 is typing", "%2 are typing", 0240 root.currentRoom.usersTyping.length, 0241 root.currentRoom.usersTyping.map(user => user.displayName).join(", ") 0242 ) : 0243 "" 0244 anchors.left: parent.left 0245 anchors.bottom: parent.bottom 0246 height: visible ? implicitHeight : 0 0247 Behavior on height { 0248 NumberAnimation { 0249 property: "height" 0250 duration: Kirigami.Units.shortDuration 0251 easing.type: Easing.OutCubic 0252 } 0253 } 0254 z: 2 0255 } 0256 0257 function goToEvent(eventID) { 0258 const index = eventToIndex(eventID) 0259 messageListView.positionViewAtIndex(index, ListView.Center) 0260 itemAtIndex(index).isTemporaryHighlighted = true 0261 } 0262 0263 HoverActions { 0264 id: hoverActions 0265 currentRoom: root.currentRoom 0266 onFocusChatBar: root.focusChatBar() 0267 } 0268 0269 onContentYChanged: { 0270 if (hoverActions.delegate) { 0271 hoverActions.delegate.setHoverActionsToDelegate(); 0272 } 0273 } 0274 0275 Connections { 0276 target: root.timelineModel 0277 0278 function onRowsInserted() { 0279 markReadIfVisibleTimer.reset() 0280 } 0281 } 0282 0283 Timer { 0284 id: markReadIfVisibleTimer 0285 running: messageListView.allUnreadVisible() && applicationWindow().active && (root.currentRoom.timelineSize > 0 || root.currentRoom.allHistoryLoaded) 0286 interval: 10000 0287 onTriggered: root.currentRoom.markAllMessagesAsRead() 0288 0289 function reset() { 0290 restart() 0291 running = Qt.binding(function() { return messageListView.allUnreadVisible() && applicationWindow().active && (root.currentRoom.timelineSize > 0 || root.currentRoom.allHistoryLoaded) }) 0292 } 0293 } 0294 0295 Rectangle { 0296 FancyEffectsContainer { 0297 id: fancyEffectsContainer 0298 anchors.fill: parent 0299 z: 100 0300 0301 enabled: Config.showFancyEffects 0302 0303 function processFancyEffectsReason(fancyEffect) { 0304 if (fancyEffect === "snowflake") { 0305 fancyEffectsContainer.showSnowEffect() 0306 } 0307 if (fancyEffect === "fireworks") { 0308 fancyEffectsContainer.showFireworksEffect() 0309 } 0310 if (fancyEffect === "confetti") { 0311 fancyEffectsContainer.showConfettiEffect() 0312 } 0313 } 0314 0315 Connections { 0316 //enabled: Config.showFancyEffects 0317 target: root.timelineModel.messageEventModel 0318 0319 function onFancyEffectsReasonFound(fancyEffect) { 0320 fancyEffectsContainer.processFancyEffectsReason(fancyEffect) 0321 } 0322 } 0323 0324 Connections { 0325 enabled: Config.showFancyEffects 0326 target: actionsHandler 0327 0328 function onShowEffect(fancyEffect) { 0329 fancyEffectsContainer.processFancyEffectsReason(fancyEffect) 0330 } 0331 } 0332 } 0333 } 0334 0335 function goToLastMessage() { 0336 root.currentRoom.markAllMessagesAsRead() 0337 // scroll to the very end, i.e to messageListView.YEnd 0338 messageListView.positionViewAtIndex(0, ListView.End) 0339 } 0340 0341 function eventToIndex(eventID) { 0342 const index = root.timelineModel.messageEventModel.eventIdToRow(eventID) 0343 if (index === -1) 0344 return -1 0345 return root.messageFilterModel.mapFromSource(root.timelineModel.index(index, 0)).row 0346 } 0347 0348 function firstVisibleIndex() { 0349 let center = messageListView.x + messageListView.width / 2; 0350 let index = -1 0351 let i = 0 0352 while (index === -1 && i < 100) { 0353 index = messageListView.indexAt(center, messageListView.y + messageListView.contentY + i); 0354 i++; 0355 } 0356 return index 0357 } 0358 0359 function lastVisibleIndex() { 0360 let center = messageListView.x + messageListView.width / 2; 0361 let index = -1 0362 let i = 0 0363 while (index === -1 && i < 100) { 0364 index = messageListView.indexAt(center, messageListView.y + messageListView.contentY + messageListView.height - i); 0365 i++ 0366 } 0367 return index; 0368 } 0369 0370 function allUnreadVisible() { 0371 let readMarkerRow = eventToIndex(root.currentRoom.readMarkerEventId) 0372 if (readMarkerRow >= 0 && readMarkerRow < firstVisibleIndex() && messageListView.atYEnd) { 0373 return true 0374 } 0375 return false 0376 } 0377 0378 function setHoverActionsToDelegate(delegate) { 0379 hoverActions.delegate = delegate 0380 } 0381 } 0382 0383 function goToLastMessage() { 0384 messageListView.goToLastMessage() 0385 } 0386 0387 function pageUp() { 0388 const newContentY = messageListView.contentY - messageListView.height / 2; 0389 const minContentY = messageListView.originY + messageListView.topMargin; 0390 messageListView.contentY = Math.max(newContentY, minContentY); 0391 messageListView.returnToBounds(); 0392 } 0393 0394 function pageDown() { 0395 const newContentY = messageListView.contentY + messageListView.height / 2; 0396 const maxContentY = messageListView.originY + messageListView.bottomMargin + messageListView.contentHeight - messageListView.height; 0397 messageListView.contentY = Math.min(newContentY, maxContentY); 0398 messageListView.returnToBounds(); 0399 } 0400 0401 function positionViewAtBeginning() { 0402 messageListView.positionViewAtBeginning() 0403 } 0404 }