matrixion/resources/qml/MessageView.qml

652 lines
24 KiB
QML
Raw Permalink Normal View History

// SPDX-FileCopyrightText: Nheko Contributors
//
2021-03-05 02:35:15 +03:00
// SPDX-License-Identifier: GPL-3.0-or-later
2021-12-13 02:43:05 +03:00
import "./components"
import "./delegates"
import "./emoji"
import "./ui"
import "./dialogs"
import Qt.labs.platform 1.1 as Platform
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.2
import QtQuick.Window 2.13
import im.nheko 1.0
Item {
id: chatRoot
property int availableWidth: width
2023-06-02 02:45:24 +03:00
property int padding: Nheko.paddingMedium
2022-10-06 22:59:59 +03:00
property string searchString: ""
2023-06-22 20:54:17 +03:00
property Room roommodel: room
2022-10-06 22:59:59 +03:00
// HACK: https://bugreports.qt.io/browse/QTBUG-83972, qtwayland cannot auto hide menu
Connections {
function onHideMenu() {
messageContextMenuC.close();
2023-10-10 01:00:17 +03:00
replyContextMenuC.close();
}
2023-06-02 02:45:24 +03:00
target: MainWindow
}
2023-06-02 02:45:24 +03:00
2023-10-18 23:33:23 +03:00
ScrollView {
id: scrollView
2023-06-02 02:45:24 +03:00
anchors.fill: parent
2023-10-18 23:33:23 +03:00
ListView {
id: chat
property int delegateMaxWidth: ((Settings.timelineMaxWidth > 100 && Settings.timelineMaxWidth < chatRoot.availableWidth) ? Settings.timelineMaxWidth : chatRoot.availableWidth) - chatRoot.padding * 2 - scrollView.effectiveScrollBarWidth
readonly property alias filteringInProgress: filteredTimeline.filteringInProgress
// reuseItems still has a few bugs, see https://bugreports.qt.io/browse/QTBUG-95105 https://bugreports.qt.io/browse/QTBUG-95107
//onModelChanged: if (room) room.sendReset()
//reuseItems: true
boundsBehavior: Flickable.StopAtBounds
displayMarginBeginning: height / 4
displayMarginEnd: height / 4
model: (filteredTimeline.filterByThread || filteredTimeline.filterByContent) ? filteredTimeline : room
//pixelAligned: true
spacing: 2
verticalLayoutDirection: ListView.BottomToTop
Component {
id: defaultMessageStyle
TimelineDefaultMessageStyle {
messageActions: messageActionsC
messageContextMenu: messageContextMenuC
replyContextMenu: replyContextMenuC
scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
}
2023-10-09 04:49:45 +03:00
}
2023-10-18 23:33:23 +03:00
Component {
id: bubbleMessageStyle
TimelineBubbleMessageStyle {
messageActions: messageActionsC
messageContextMenu: messageContextMenuC
replyContextMenu: replyContextMenuC
scrolledToThis: eventId === room.scrollTarget && (y + height > chat.y + chat.contentY && y < chat.y + chat.height + chat.contentY)
}
2023-10-09 22:28:39 +03:00
}
2023-06-02 02:45:24 +03:00
2023-10-18 23:33:23 +03:00
delegate: Settings.bubbles ? bubbleMessageStyle : defaultMessageStyle
footer: Item {
anchors.horizontalCenter: parent.horizontalCenter
2023-06-02 02:45:24 +03:00
anchors.margins: Nheko.paddingLarge
2023-10-18 23:33:23 +03:00
// hacky, but works
height: loadingSpinner.height + 2 * Nheko.paddingLarge
visible: (room && room.paginationInProgress) || chat.filteringInProgress
Spinner {
id: loadingSpinner
anchors.centerIn: parent
anchors.margins: Nheko.paddingLarge
foreground: palette.mid
running: (room && room.paginationInProgress) || chat.filteringInProgress
z: 3
}
2023-06-02 02:45:24 +03:00
}
2023-10-18 23:33:23 +03:00
Window.onActiveChanged: readTimer.running = Window.active
onCountChanged: {
// Mark timeline as read
if (atYEnd && room)
2023-06-02 02:45:24 +03:00
model.currentIndex = 0;
2022-02-20 12:09:22 +03:00
}
2023-10-18 23:33:23 +03:00
TimelineFilter {
id: filteredTimeline
2023-10-18 23:33:23 +03:00
filterByContent: chatRoot.searchString
filterByThread: room ? room.thread : ""
source: room
}
Control {
id: messageActionsC
property Item attached: null
// use comma to update on scroll
property alias model: row.model
hoverEnabled: true
padding: Nheko.paddingSmall
visible: Settings.buttonsInTimeline && !!attached && (attached.hovered || hovered)
z: 10
parent: chat.contentItem
anchors.bottom: attached?.top
anchors.right: attached?.right
background: Rectangle {
border.color: palette.buttonText
border.width: 1
color: palette.window
radius: padding
}
contentItem: RowLayout {
id: row
property var model
spacing: messageActionsC.padding
Repeater {
model: Settings.recentReactions
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
delegate: AbstractButton {
id: button
property color buttonTextColor: palette.buttonText
property color highlightColor: palette.highlight
required property string modelData
property bool showImage: modelData.startsWith("mxc://")
//Layout.preferredHeight: fontMetrics.height
Layout.alignment: Qt.AlignBottom
focusPolicy: Qt.NoFocus
height: showImage ? 16 : buttonText.implicitHeight
implicitHeight: showImage ? 16 : buttonText.implicitHeight
implicitWidth: showImage ? 16 : buttonText.implicitWidth
width: showImage ? 16 : buttonText.implicitWidth
onClicked: {
room.input.reaction(row.model.eventId, modelData);
TimelineManager.focusMessageInput();
}
Label {
id: buttonText
anchors.centerIn: parent
color: button.hovered ? button.highlightColor : button.buttonTextColor
font.family: Settings.emojiFont
horizontalAlignment: Text.AlignHCenter
padding: 0
text: button.modelData
verticalAlignment: Text.AlignVCenter
visible: !button.showImage
}
Image {
id: buttonImg
// Workaround, can't get icon.source working for now...
anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: button.showImage ? (button.modelData.replace("mxc://", "image://MxcImage/") + "?scale") : ""
sourceSize.height: button.height
sourceSize.width: button.width
}
NhekoCursorShape {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
}
Ripple {
color: Qt.rgba(buttonTextColor.r, buttonTextColor.g, buttonTextColor.b, 0.5)
}
}
}
2023-10-18 23:33:23 +03:00
ImageButton {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Edit")
ToolTip.visible: hovered
buttonTextColor: palette.buttonText
hoverEnabled: true
image: ":/icons/icons/ui/edit.svg"
visible: !!row.model && row.model.isEditable
width: 16
onClicked: {
if (row.model.isEditable)
2023-06-02 02:45:24 +03:00
room.edit = row.model.eventId;
2023-10-18 23:33:23 +03:00
}
}
2023-10-18 23:33:23 +03:00
ImageButton {
id: reactButton
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("React")
ToolTip.visible: hovered
hoverEnabled: true
image: ":/icons/icons/ui/smile-add.svg"
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
width: 16
onClicked: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(reactButton, room.roomId, function (plaintext, markdown) {
2023-06-02 02:45:24 +03:00
var event_id = row.model ? row.model.eventId : "";
room.input.reaction(event_id, plaintext);
TimelineManager.focusMessageInput();
})
}
2023-10-18 23:33:23 +03:00
ImageButton {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: (row.model && row.model.threadId) ? qsTr("Reply in thread") : qsTr("New thread")
ToolTip.visible: hovered
hoverEnabled: true
image: (row.model && row.model.threadId) ? ":/icons/icons/ui/thread.svg" : ":/icons/icons/ui/new-thread.svg"
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
width: 16
onClicked: room.thread = (row.model.threadId || row.model.eventId)
}
ImageButton {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Reply")
ToolTip.visible: hovered
hoverEnabled: true
image: ":/icons/icons/ui/reply.svg"
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
width: 16
onClicked: room.reply = row.model.eventId
}
ImageButton {
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Go to message")
ToolTip.visible: hovered
buttonTextColor: palette.buttonText
hoverEnabled: true
image: ":/icons/icons/ui/go-to.svg"
visible: !!row.model && filteredTimeline.filterByContent
width: 16
onClicked: {
topBar.searchString = "";
room.showEvent(row.model.eventId);
}
}
ImageButton {
id: optionsButton
2023-10-18 23:33:23 +03:00
ToolTip.delay: Nheko.tooltipDelay
ToolTip.text: qsTr("Options")
ToolTip.visible: hovered
hoverEnabled: true
image: ":/icons/icons/ui/options.svg"
width: 16
2023-06-02 02:45:24 +03:00
2023-10-18 23:33:23 +03:00
onClicked: messageContextMenuC.show(row.model.eventId, row.model.threadId, row.model.type, row.model.isSender, row.model.isEncrypted, row.model.isEditable, "", row.model.body, optionsButton)
}
}
}
2023-10-18 23:33:23 +03:00
Shortcut {
sequence: StandardKey.MoveToPreviousPage
2023-06-02 02:45:24 +03:00
2023-10-18 23:33:23 +03:00
onActivated: {
chat.contentY = chat.contentY - chat.height * 0.9;
chat.returnToBounds();
}
}
2023-10-18 23:33:23 +03:00
Shortcut {
sequence: StandardKey.MoveToNextPage
2023-06-02 02:45:24 +03:00
2023-10-18 23:33:23 +03:00
onActivated: {
chat.contentY = chat.contentY + chat.height * 0.9;
chat.returnToBounds();
}
}
2023-10-18 23:33:23 +03:00
Shortcut {
sequence: StandardKey.Cancel
2023-06-02 02:45:24 +03:00
2023-10-18 23:33:23 +03:00
onActivated: {
if (room.input.uploads.length > 0)
room.input.declineUploads();
2023-10-18 23:33:23 +03:00
else if (room.reply)
room.reply = undefined;
2023-10-18 23:33:23 +03:00
else if (room.edit)
room.edit = undefined;
2023-10-18 23:33:23 +03:00
else
2023-06-02 02:45:24 +03:00
room.thread = undefined;
2023-10-18 23:33:23 +03:00
TimelineManager.focusMessageInput();
}
}
2023-10-18 23:33:23 +03:00
// These shortcuts use the room timeline because switching to threads and out is annoying otherwise.
// Better solution welcome.
Shortcut {
sequence: "Alt+Up"
2023-06-02 02:45:24 +03:00
2023-10-18 23:33:23 +03:00
onActivated: room.reply = room.indexToId(room.reply ? room.idToIndex(room.reply) + 1 : 0)
}
Shortcut {
sequence: "Alt+Down"
2023-06-02 02:45:24 +03:00
2023-10-18 23:33:23 +03:00
onActivated: {
var idx = room.reply ? room.idToIndex(room.reply) - 1 : -1;
room.reply = idx >= 0 ? room.indexToId(idx) : null;
}
}
2023-10-18 23:33:23 +03:00
Shortcut {
sequence: "Alt+F"
onActivated: {
if (room.reply) {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(room.reply);
forwardMess.open();
room.reply = null;
timelineRoot.destroyOnClose(forwardMess);
}
}
}
2023-10-18 23:33:23 +03:00
Shortcut {
sequence: "Ctrl+E"
2023-06-02 02:45:24 +03:00
2023-10-18 23:33:23 +03:00
onActivated: {
room.edit = room.reply;
}
}
2023-10-18 23:33:23 +03:00
Timer {
id: readTimer
2023-10-18 23:33:23 +03:00
interval: 1000
2023-06-02 02:45:24 +03:00
2023-10-18 23:33:23 +03:00
// force current read index to update
onTriggered: {
if (room)
2023-06-02 02:45:24 +03:00
room.setCurrentIndex(room.currentIndex);
2023-10-18 23:33:23 +03:00
}
}
2022-02-20 12:09:22 +03:00
}
}
2023-10-18 23:33:23 +03:00
Platform.Menu {
id: messageContextMenuC
property string eventId
property int eventType
property bool isEditable
2023-06-02 02:45:24 +03:00
property bool isEncrypted
property bool isSender
2023-06-02 02:45:24 +03:00
property string link
property string text
property string threadId
2022-09-30 04:27:05 +03:00
function show(eventId_, threadId_, eventType_, isSender_, isEncrypted_, isEditable_, link_, text_, showAt_) {
eventId = eventId_;
2022-09-30 04:27:05 +03:00
threadId = threadId_;
eventType = eventType_;
isEncrypted = isEncrypted_;
isEditable = isEditable_;
isSender = isSender_;
if (text_)
2023-06-02 02:45:24 +03:00
text = text_;
else
2023-06-02 02:45:24 +03:00
text = "";
if (link_)
2023-06-02 02:45:24 +03:00
link = link_;
else
2023-06-02 02:45:24 +03:00
link = "";
if (showAt_)
2023-06-02 02:45:24 +03:00
open(showAt_);
else
2023-06-02 02:45:24 +03:00
open();
}
Component {
id: removeReason
2023-06-02 02:45:24 +03:00
InputDialog {
id: removeReasonDialog
property string eventId
prompt: qsTr("Enter reason for removal or hit enter for no reason:")
2023-06-02 02:45:24 +03:00
title: qsTr("Reason for removal")
onAccepted: function (text) {
room.redactEvent(eventId, text);
}
}
}
Platform.MenuItem {
2023-06-02 02:45:24 +03:00
enabled: visible
text: qsTr("Go to &message")
visible: filteredTimeline.filterByContent
onTriggered: function () {
topBar.searchString = "";
room.showEvent(messageContextMenuC.eventId);
}
2023-06-02 02:45:24 +03:00
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Copy")
visible: messageContextMenuC.text
2023-06-02 02:45:24 +03:00
onTriggered: Clipboard.text = messageContextMenuC.text
}
Platform.MenuItem {
enabled: visible
text: qsTr("Copy &link location")
visible: messageContextMenuC.link
2023-06-02 02:45:24 +03:00
onTriggered: Clipboard.text = messageContextMenuC.link
}
Platform.MenuItem {
id: reactionOption
text: qsTr("Re&act")
2023-06-02 02:45:24 +03:00
visible: room ? room.permissions.canSend(MtxEvent.Reaction) : false
2023-06-02 02:45:24 +03:00
onTriggered: emojiPopup.visible ? emojiPopup.close() : emojiPopup.show(null, room.roomId, function (plaintext, markdown) {
room.input.reaction(messageContextMenuC.eventId, plaintext);
2023-06-02 02:45:24 +03:00
TimelineManager.focusMessageInput();
})
}
Platform.MenuItem {
text: qsTr("Repl&y")
2023-06-02 02:45:24 +03:00
visible: room ? room.permissions.canSend(MtxEvent.TextMessage) : false
onTriggered: room.reply = (messageContextMenuC.eventId)
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Edit")
visible: messageContextMenuC.isEditable && (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
2023-06-02 02:45:24 +03:00
onTriggered: room.edit = (messageContextMenuC.eventId)
2022-09-30 04:27:05 +03:00
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Thread")
2023-06-02 02:45:24 +03:00
visible: (room ? room.permissions.canSend(MtxEvent.TextMessage) : false)
onTriggered: room.thread = (messageContextMenuC.threadId || messageContextMenuC.eventId)
}
2021-12-11 08:10:41 +03:00
Platform.MenuItem {
enabled: visible
text: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? qsTr("Un&pin") : qsTr("&Pin")
2023-06-02 02:45:24 +03:00
visible: (room ? room.permissions.canChange(MtxEvent.PinnedEvents) : false)
onTriggered: visible && room.pinnedMessages.includes(messageContextMenuC.eventId) ? room.unpin(messageContextMenuC.eventId) : room.pin(messageContextMenuC.eventId)
2021-12-11 08:10:41 +03:00
}
Platform.MenuItem {
2022-09-30 04:27:05 +03:00
text: qsTr("&Read receipts")
2023-06-02 02:45:24 +03:00
onTriggered: room.showReadReceipts(messageContextMenuC.eventId)
}
Platform.MenuItem {
text: qsTr("&Forward")
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker || messageContextMenuC.eventType == MtxEvent.TextMessage || messageContextMenuC.eventType == MtxEvent.LocationMessage || messageContextMenuC.eventType == MtxEvent.EmoteMessage || messageContextMenuC.eventType == MtxEvent.NoticeMessage
2023-06-02 02:45:24 +03:00
onTriggered: {
var forwardMess = forwardCompleterComponent.createObject(timelineRoot);
forwardMess.setMessageEventId(messageContextMenuC.eventId);
forwardMess.open();
timelineRoot.destroyOnClose(forwardMess);
}
}
Platform.MenuItem {
text: qsTr("&Mark as read")
}
Platform.MenuItem {
text: qsTr("View raw message")
2023-06-02 02:45:24 +03:00
onTriggered: room.viewRawMessage(messageContextMenuC.eventId)
}
Platform.MenuItem {
enabled: visible
text: qsTr("View decrypted raw message")
2023-06-02 02:45:24 +03:00
// TODO(Nico): Fix this still being iterated over, when using keyboard to select options
visible: messageContextMenuC.isEncrypted
2023-06-02 02:45:24 +03:00
onTriggered: room.viewDecryptedRawMessage(messageContextMenuC.eventId)
}
Platform.MenuItem {
text: qsTr("Remo&ve message")
visible: (room ? room.permissions.canRedact() : false) || messageContextMenuC.isSender
2023-06-02 02:45:24 +03:00
onTriggered: function () {
var dialog = removeReason.createObject(timelineRoot);
dialog.eventId = messageContextMenuC.eventId;
dialog.show();
dialog.forceActiveFocus();
timelineRoot.destroyOnClose(dialog);
}
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Save as")
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker
2023-06-02 02:45:24 +03:00
onTriggered: room.saveMedia(messageContextMenuC.eventId)
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Open in external program")
visible: messageContextMenuC.eventType == MtxEvent.ImageMessage || messageContextMenuC.eventType == MtxEvent.VideoMessage || messageContextMenuC.eventType == MtxEvent.AudioMessage || messageContextMenuC.eventType == MtxEvent.FileMessage || messageContextMenuC.eventType == MtxEvent.Sticker
2023-06-02 02:45:24 +03:00
onTriggered: room.openMedia(messageContextMenuC.eventId)
}
Platform.MenuItem {
enabled: visible
text: qsTr("Copy link to eve&nt")
visible: messageContextMenuC.eventId
2023-06-02 02:45:24 +03:00
onTriggered: room.copyLinkToEvent(messageContextMenuC.eventId)
}
}
Component {
id: forwardCompleterComponent
ForwardCompleter {
}
}
2021-08-25 17:10:55 +03:00
Platform.Menu {
2023-10-10 01:00:17 +03:00
id: replyContextMenuC
2021-08-25 17:10:55 +03:00
2022-03-24 03:35:42 +03:00
property string eventId
2023-06-02 02:45:24 +03:00
property string link
property string text
2021-08-25 17:10:55 +03:00
2022-03-24 03:35:42 +03:00
function show(text_, link_, eventId_) {
2021-08-25 17:10:55 +03:00
text = text_;
link = link_;
2022-03-24 03:35:42 +03:00
eventId = eventId_;
2021-08-25 17:10:55 +03:00
open();
}
Platform.MenuItem {
enabled: visible
text: qsTr("&Copy")
2023-10-10 01:00:17 +03:00
visible: replyContextMenuC.text
2023-06-02 02:45:24 +03:00
2023-10-10 01:00:17 +03:00
onTriggered: Clipboard.text = replyContextMenuC.text
2021-08-25 17:10:55 +03:00
}
Platform.MenuItem {
enabled: visible
text: qsTr("Copy &link location")
2023-10-10 01:00:17 +03:00
visible: replyContextMenuC.link
2023-06-02 02:45:24 +03:00
2023-10-10 01:00:17 +03:00
onTriggered: Clipboard.text = replyContextMenuC.link
2021-08-25 17:10:55 +03:00
}
Platform.MenuItem {
enabled: visible
2021-09-14 00:17:03 +03:00
text: qsTr("&Go to quoted message")
2023-06-02 02:45:24 +03:00
visible: true
2023-10-10 01:00:17 +03:00
onTriggered: room.showEvent(replyContextMenuC.eventId)
2021-08-25 17:10:55 +03:00
}
}
2022-03-30 00:11:25 +03:00
RoundButton {
id: toEndButton
2023-06-02 02:45:24 +03:00
2022-03-30 00:11:25 +03:00
property int fullWidth: 40
2023-06-02 02:45:24 +03:00
2022-03-30 00:11:25 +03:00
flat: true
2023-06-02 02:45:24 +03:00
height: width
hoverEnabled: true
2023-06-02 02:45:24 +03:00
radius: width / 2
width: 0
background: Rectangle {
border.color: toEndButton.hovered ? palette.highlight : palette.buttonText
border.width: 1
2023-06-02 02:45:24 +03:00
color: toEndButton.down ? palette.highlight : palette.button
opacity: enabled ? 1 : 0.3
radius: toEndButton.radius
}
states: [
State {
name: ""
2023-06-02 02:45:24 +03:00
PropertyChanges {
target: toEndButton
width: 0
}
},
State {
name: "shown"
when: !chat.atYEnd
2023-06-02 02:45:24 +03:00
PropertyChanges {
target: toEndButton
width: toEndButton.fullWidth
}
}
]
transitions: Transition {
from: ""
reversible: true
2023-06-02 02:45:24 +03:00
to: "shown"
2022-03-30 00:11:25 +03:00
SequentialAnimation {
2023-06-02 02:45:24 +03:00
PauseAnimation {
duration: 500
}
2022-03-30 00:11:25 +03:00
PropertyAnimation {
duration: 200
2023-06-02 02:45:24 +03:00
easing.type: Easing.InOutQuad
properties: "width"
target: toEndButton
2022-03-30 00:11:25 +03:00
}
}
}
2023-06-02 02:45:24 +03:00
onClicked: function () {
chat.positionViewAtBeginning();
TimelineManager.focusMessageInput();
}
anchors {
bottom: parent.bottom
bottomMargin: Nheko.paddingMedium + (fullWidth - width) / 2
2023-10-18 23:33:23 +03:00
right: parent.left
2023-06-02 02:45:24 +03:00
rightMargin: Nheko.paddingMedium + (fullWidth - width) / 2
}
Image {
id: buttonImg
anchors.fill: parent
anchors.margins: Nheko.paddingMedium
fillMode: Image.PreserveAspectFit
source: "image://colorimage/:/icons/icons/ui/download.svg?" + (toEndButton.down ? palette.highlightedText : palette.buttonText)
}
2022-03-30 00:11:25 +03:00
}
}