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