From 101a8b9ec3cd82cf55c45c76a873ae86bee19152 Mon Sep 17 00:00:00 2001 From: James Graham Date: Sun, 11 May 2025 11:11:52 +0100 Subject: [PATCH] Yeet HoverActions into the sun Replace HoverActions with an inline action component that appears on hover. There are only actions for reply and react if there is space the overflow button opens the normal message menu. NOTE: the most recent update changes things slightly, from the images below the buttons are now top aligned because of potentially hige messages. The actions are also now disabled for compact mode as they never really made sense there anyway. The menu now has all options so no one is missing out. For normal messages ![image](/uploads/b8679eb09c9190404fc84f01e14169af/image.png){width=419 height=138} When space is limited ![image](/uploads/ecd7c725ea2526689e586a2d786f389e/image.png){width=411 height=130} User messages ![image](/uploads/767ef09f6650a5fb6abf3a49ef9f9b90/image.png){width=296 height=114} BUG: 503784 --- snapcraft.yaml | 2 +- src/libneochat/neochatroom.cpp | 33 ++++ src/libneochat/neochatroom.h | 11 ++ src/timeline/CMakeLists.txt | 2 +- src/timeline/DelegateContextMenu.qml | 13 ++ src/timeline/HoverActions.qml | 181 -------------------- src/timeline/MessageDelegate.qml | 28 +-- src/timeline/MessageDelegateContextMenu.qml | 2 + src/timeline/QuickActions.qml | 90 ++++++++++ src/timeline/TimelineView.qml | 12 -- src/timeline/messagedelegate.cpp | 71 +++++++- src/timeline/messagedelegate.h | 16 ++ 12 files changed, 239 insertions(+), 222 deletions(-) delete mode 100644 src/timeline/HoverActions.qml create mode 100644 src/timeline/QuickActions.qml diff --git a/snapcraft.yaml b/snapcraft.yaml index b91239726..ef64694a8 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -101,7 +101,7 @@ parts: - olm - qtkeychain source: https://github.com/quotient-im/libQuotient.git - source-tag: 0.9.1 + source-tag: 0.9.2 source-depth: 1 plugin: cmake build-environment: diff --git a/src/libneochat/neochatroom.cpp b/src/libneochat/neochatroom.cpp index 43d47be42..b86ea65ef 100644 --- a/src/libneochat/neochatroom.cpp +++ b/src/libneochat/neochatroom.cpp @@ -37,6 +37,9 @@ #include #include #include +#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1) +#include +#endif #include "chatbarcache.h" #include "clipboard.h" @@ -1708,4 +1711,34 @@ bool NeoChatRoom::isEventPinned(const QString &eventId) const return pinnedEventIds().contains(eventId); } +bool NeoChatRoom::eventIsThreaded(const QString &eventId) const +{ + const auto event = eventCast(getEvent(eventId).first); + if (event == nullptr) { + return false; + } + +#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1) + return event->isThreaded() || threads().contains(eventId); +#else + return event->isThreaded(); +#endif +} + +QString NeoChatRoom::rootIdForThread(const QString &eventId) const +{ + const auto event = eventCast(getEvent(eventId).first); + if (event == nullptr) { + return {}; + } + + auto rootId = event->threadRootEventId(); +#if Quotient_VERSION_MINOR > 9 || (Quotient_VERSION_MINOR == 9 && Quotient_VERSION_PATCH > 1) + if (rootId.isEmpty() && threads().contains(eventId)) { + rootId = event->id(); + } +#endif + return rootId; +} + #include "moc_neochatroom.cpp" diff --git a/src/libneochat/neochatroom.h b/src/libneochat/neochatroom.h index 3c70d5197..c57b8922f 100644 --- a/src/libneochat/neochatroom.h +++ b/src/libneochat/neochatroom.h @@ -576,6 +576,17 @@ public: */ Q_INVOKABLE bool isEventPinned(const QString &eventId) const; + /** + * @return True if the given @p eventId is threaded. + */ + Q_INVOKABLE bool eventIsThreaded(const QString &eventId) const; + + /** + * @return Returns the thread root ID for @p eventId as a string. The string + * is empty if the event is not part of a thread. + */ + Q_INVOKABLE QString rootIdForThread(const QString &eventId) const; + private: bool m_visible = false; diff --git a/src/timeline/CMakeLists.txt b/src/timeline/CMakeLists.txt index dea1dc278..b3d43f7a0 100644 --- a/src/timeline/CMakeLists.txt +++ b/src/timeline/CMakeLists.txt @@ -7,7 +7,6 @@ ecm_add_qml_module(Timeline GENERATE_PLUGIN_SOURCE OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/timeline QML_FILES TimelineView.qml - HoverActions.qml EventDelegate.qml HiddenDelegate.qml MessageDelegate.qml @@ -20,6 +19,7 @@ ecm_add_qml_module(Timeline GENERATE_PLUGIN_SOURCE Bubble.qml AvatarFlow.qml SectionDelegate.qml + QuickActions.qml BaseMessageComponentChooser.qml MessageComponentChooser.qml ReplyMessageComponentChooser.qml diff --git a/src/timeline/DelegateContextMenu.qml b/src/timeline/DelegateContextMenu.qml index 233ff03c5..88c54700c 100644 --- a/src/timeline/DelegateContextMenu.qml +++ b/src/timeline/DelegateContextMenu.qml @@ -104,6 +104,19 @@ KirigamiComponents.ConvergentContextMenu { } } + component ReplyThreadMessageAction: QQC2.Action { + text: i18nc("@action:button", "Reply in Thread") + icon.name: "dialog-messages" + onTriggered: { + currentRoom.threadCache.replyId = ""; + currentRoom.threadCache.threadId = currentRoom.eventIsThreaded(root.eventId) ? currentRoom.rootIdForThread(root.eventId) : root.eventId; + currentRoom.mainCache.clearRelations(); + currentRoom.editCache.clearRelations(); + RoomManager.requestFullScreenClose(); + } + } + + component ReportMessageAction: Kirigami.Action { text: i18nc("@action:button 'Report' as in 'Report this event to the administrators'", "Report") icon.name: "dialog-warning-symbolic" diff --git a/src/timeline/HoverActions.qml b/src/timeline/HoverActions.qml deleted file mode 100644 index 45f0b3243..000000000 --- a/src/timeline/HoverActions.qml +++ /dev/null @@ -1,181 +0,0 @@ -// SPDX-FileCopyrightText: 2023 James Graham -// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - -import QtQuick -import QtQuick.Controls as QQC2 -import QtQuick.Layouts - -import org.kde.kirigami as Kirigami - -import org.kde.neochat -import org.kde.neochat.chatbar - -/** - * @brief A component that provides a set of actions when a message is hovered in the timeline. - * - * There is also an icon to show that a message has come from a verified device in - * encrypted chats. - */ -QQC2.Control { - id: root - - /** - * @brief The current message delegate the actions are being shown on. - */ - property var delegate: null - - /** - * @brief The current room that user is viewing. - */ - required property NeoChatRoom currentRoom - - /** - * @brief Whether the actions should be shown. - */ - readonly property bool showActions: delegate && delegate.hovered - - /** - * @brief Request that the chat bar be focussed. - */ - signal focusChatBar - - topPadding: 0 - bottomPadding: 0 - leftPadding: 0 - rightPadding: 0 - - visible: (root.hovered || root.showActions || showActionsTimer.running) && !Kirigami.Settings.isMobile && (!root.delegate.isThreaded || !NeoChatConfig.threads) - onVisibleChanged: { - if (visible) { - // HACK: delay disapearing by 200ms, otherwise this can create some glitches - // See https://invent.kde.org/network/neochat/-/issues/333 - showActionsTimer.restart(); - } - } - Timer { - id: showActionsTimer - interval: 200 - } - - function updatePosition(): void { - if (delegate) { - root.x = delegate.contentItem.x + delegate.bubbleWidth - root.implicitWidth - Kirigami.Units.largeSpacing; - root.y = delegate.mapToItem(parent, 0, 0).y + delegate.bubbleY - height + Kirigami.Units.smallSpacing; - } - } - - onDelegateChanged: updatePosition() - onWidthChanged: updatePosition() - - contentItem: RowLayout { - id: actionsLayout - - spacing: Kirigami.Units.smallSpacing - - Item { - Layout.fillWidth: true - } - - Kirigami.Icon { - source: "security-high" - width: height - height: root.height - visible: root.delegate && root.delegate.verified - HoverHandler { - id: hover - } - - QQC2.ToolTip.text: i18n("This message was sent from a verified device") - QQC2.ToolTip.visible: hover.hovered - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - } - - QQC2.Button { - text: i18n("React") - icon.name: "preferences-desktop-emoticons" - onClicked: emojiDialog.open() - display: QQC2.ToolButton.IconOnly - - QQC2.ToolTip.text: text - QQC2.ToolTip.visible: hovered - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - } - - QQC2.Button { - visible: root.delegate && root.delegate.isEditable && !root.currentRoom.readOnly - text: i18n("Edit") - icon.name: "document-edit" - display: QQC2.Button.IconOnly - - onClicked: { - root.currentRoom.editCache.editId = root.delegate.eventId; - root.currentRoom.mainCache.replyId = ""; - root.currentRoom.mainCache.threadId = ""; - } - - QQC2.ToolTip.text: text - QQC2.ToolTip.visible: hovered - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - } - - QQC2.Button { - visible: !root.currentRoom.readOnly - text: i18n("Reply") - icon.name: "mail-replied-symbolic" - display: QQC2.Button.IconOnly - onClicked: { - root.currentRoom.mainCache.replyId = root.delegate.eventId; - root.currentRoom.editCache.editId = ""; - root.currentRoom.mainCache.threadId = ""; - root.focusChatBar(); - } - - QQC2.ToolTip.text: text - QQC2.ToolTip.visible: hovered - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - } - - QQC2.Button { - visible: NeoChatConfig.threads && !root.currentRoom.readOnly && !root.delegate?.isPoll - text: i18n("Reply in Thread") - icon.name: "dialog-messages" - display: QQC2.Button.IconOnly - onClicked: { - root.currentRoom.threadCache.replyId = ""; - root.currentRoom.threadCache.threadId = root.delegate.isThreaded ? root.delegate.threadRoot : root.delegate.eventId; - root.currentRoom.mainCache.clearRelations(); - root.currentRoom.editCache.clearRelations(); - root.focusChatBar(); - } - - QQC2.ToolTip.text: text - QQC2.ToolTip.visible: hovered - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - } - - QQC2.Button { - visible: (root.delegate?.isPoll ?? false) && !ContentProvider.handlerForPoll(root.currentRoom, root.delegate.eventId).hasEnded - text: i18n("End Poll") - icon.name: "gtk-stop" - display: QQC2.ToolButton.IconOnly - onClicked: root.currentRoom.poll(root.delegate.eventId).endPoll() - - QQC2.ToolTip.text: text - QQC2.ToolTip.visible: hovered - QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay - } - - EmojiDialog { - id: emojiDialog - currentRoom: root.currentRoom - showQuickReaction: true - showStickers: false - onChosen: emoji => { - root.currentRoom.toggleReaction(root.delegate.eventId, emoji); - if (!Kirigami.Settings.isMobile) { - root.focusChatBar(); - } - } - } - } -} diff --git a/src/timeline/MessageDelegate.qml b/src/timeline/MessageDelegate.qml index 6becbca15..12a7d03de 100644 --- a/src/timeline/MessageDelegate.qml +++ b/src/timeline/MessageDelegate.qml @@ -89,20 +89,6 @@ MessageDelegateBase { */ required property bool verified - /** - * @brief The y position of the message bubble. - * - * @note Used for positioning the hover actions. - */ - readonly property alias bubbleY: bubble.y - - /** - * @brief The width of the message bubble. - * - * @note Used for sizing the hover actions. - */ - readonly property alias bubbleWidth: bubble.width - /** * @brief Open the any message media externally. */ @@ -202,17 +188,9 @@ MessageDelegateBase { radius: Kirigami.Units.cornerRadius } - // show hover actions - onHoveredChanged: { - if (hovered && !Kirigami.Settings.isMobile) { - root.setHoverActionsToDelegate(); - } - } - - function setHoverActionsToDelegate() { - if (ListView.view.setHoverActionsToDelegate) { - ListView.view.setHoverActionsToDelegate(root); - } + quickActionComponent: QuickActions { + room: root.room + eventId: root.eventId } QtObject { diff --git a/src/timeline/MessageDelegateContextMenu.qml b/src/timeline/MessageDelegateContextMenu.qml index a2b13cff6..17a096675 100644 --- a/src/timeline/MessageDelegateContextMenu.qml +++ b/src/timeline/MessageDelegateContextMenu.qml @@ -50,6 +50,8 @@ DelegateContextMenu { DelegateContextMenu.ReplyMessageAction {} + DelegateContextMenu.ReplyThreadMessageAction {} + QQC2.Action { text: i18nc("@action:inmenu As in 'Forward this message'", "Forward…") icon.name: "mail-forward-symbolic" diff --git a/src/timeline/QuickActions.qml b/src/timeline/QuickActions.qml new file mode 100644 index 000000000..c7334d016 --- /dev/null +++ b/src/timeline/QuickActions.qml @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami + +import org.kde.neochat + +RowLayout { + id: root + + /** + * @brief The NeoChatRoom the delegate is being displayed in. + */ + required property NeoChatRoom room + + /** + * @brief The matrix ID of the message event. + */ + required property string eventId + + property real availableWidth: 0.0 + + property bool reacting: false + + QQC2.Button { + id: reactButton + visible: root.availableWidth > overflowButton.implicitWidth + root.spacing + reactButton.implicitWidth + text: i18n("React") + icon.name: "preferences-desktop-emoticons" + display: QQC2.ToolButton.IconOnly + onClicked: { + var dialog = emojiDialog.createObject(reactButton); + dialog.chosen.connect(emoji => { + root.reacting = false; + root.room.toggleReaction(root.eventId, emoji); + if (!Kirigami.Settings.isMobile) { + // root.focusChatBar(); + } + }); + dialog.closed.connect(() => { + root.reacting = false; + }) + root.reacting = true; + dialog.open(); + } + + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + + Component { + id: emojiDialog + EmojiDialog { + currentRoom: root.room + showQuickReaction: true + } + } + } + QQC2.Button { + id: replyButton + visible: !root.room.readOnly && root.availableWidth > overflowButton.implicitWidth + reactButton.implicitWidth + replyButton.implicitWidth + root.spacing + text: i18n("Reply") + icon.name: "mail-replied-symbolic" + display: QQC2.Button.IconOnly + onClicked: { + root.room.mainCache.replyId = root.eventId; + root.room.editCache.editId = ""; + root.room.mainCache.threadId = ""; + } + + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + } + QQC2.Button { + id: overflowButton + text: i18n("Message menu") + icon.name: "overflow-menu" + onClicked: _private.showMessageMenu() + display: QQC2.ToolButton.IconOnly + + QQC2.ToolTip.text: text + QQC2.ToolTip.visible: hovered + QQC2.ToolTip.delay: Kirigami.Units.toolTipDelay + } +} diff --git a/src/timeline/TimelineView.qml b/src/timeline/TimelineView.qml index 706f80d04..a818e00b5 100644 --- a/src/timeline/TimelineView.qml +++ b/src/timeline/TimelineView.qml @@ -288,14 +288,6 @@ QQC2.ScrollView { itemAtIndex(index).isTemporaryHighlighted = true; } - HoverActions { - id: hoverActions - currentRoom: root.currentRoom - onFocusChatBar: root.focusChatBar() - } - - onContentYChanged: hoverActions.updatePosition() - Connections { target: root.timelineModel @@ -360,10 +352,6 @@ QQC2.ScrollView { } return false; } - - function setHoverActionsToDelegate(delegate) { - hoverActions.delegate = delegate; - } } function goToLastMessage() { diff --git a/src/timeline/messagedelegate.cpp b/src/timeline/messagedelegate.cpp index 35d4293e1..c3cc70648 100644 --- a/src/timeline/messagedelegate.cpp +++ b/src/timeline/messagedelegate.cpp @@ -120,7 +120,7 @@ void MessageDelegateBase::setPercentageValues(bool fillWidth) m_contentSizeHelper.setStartPercentWidth(100); m_contentSizeHelper.setEndPercentWidth(100); } else { - m_contentSizeHelper.setStartPercentWidth(90); + m_contentSizeHelper.setStartPercentWidth(85); m_contentSizeHelper.setEndPercentWidth(60); } } @@ -402,6 +402,7 @@ void MessageDelegateBase::setCompactMode(bool compactMode) Q_EMIT maxContentWidthChanged(); updateBackground(); + updateQuickAction(); } void MessageDelegateBase::updateBackground() @@ -435,6 +436,54 @@ void MessageDelegateBase::updateBackground() } } +QQmlComponent *MessageDelegateBase::quickActionComponent() const +{ + return m_quickActionComponent; +} + +void MessageDelegateBase::setQuickActionComponent(QQmlComponent *quickActionComponent) +{ + if (quickActionComponent == m_quickActionComponent) { + return; + } + m_quickActionComponent = quickActionComponent; + Q_EMIT quickActionComponentChanged(); + + updateQuickAction(); +} + +void MessageDelegateBase::updateQuickAction() +{ + if (m_quickActionComponent && !m_compactMode && m_hovered && !m_quickActionItem && !m_quickActionIncubating) { + const auto quickActionIncubator = new MessageObjectIncubator( + m_objectInitialCallback, + [this](MessageObjectIncubator *incubator) { + if (!incubator) { + return; + } + + const auto quickActionObject = qobject_cast(incubator->object()); + if (quickActionObject) { + if (!m_compactMode) { + m_quickActionItem = quickActionObject; + connect(m_quickActionItem, SIGNAL(reactingChanged()), this, SLOT(updateQuickAction())); + } else { + cleanupItem(quickActionObject); + } + markAsDirty(); + } + cleanupIncubator(incubator); + m_quickActionIncubating = false; + }, + m_errorCallback); + m_quickActionComponent->create(*quickActionIncubator, qmlContext(m_quickActionComponent)); + m_quickActionIncubating = true; + } else if (m_quickActionItem && !m_hovered && !m_quickActionItem->property("reacting").toBool()) { + cleanupItem(m_quickActionItem); + markAsDirty(); + } +} + bool MessageDelegateBase::showLocalMessagesOnRight() const { return m_showLocalMessagesOnRight; @@ -462,6 +511,7 @@ void MessageDelegateBase::updateImplicitHeight() } qreal avatarHeight = 0.0; qreal contentHeight = 0.0; + qreal quickActionHeight = 0.0; if (showAvatar() && m_avatarItem) { m_avatarItem->setImplicitWidth(m_avatarSize); m_avatarItem->setImplicitHeight(m_avatarSize); @@ -470,7 +520,10 @@ void MessageDelegateBase::updateImplicitHeight() if (m_contentItem) { contentHeight = m_contentItem->implicitHeight(); } - implicitHeight += std::max(avatarHeight, contentHeight); + if (m_quickActionItem) { + quickActionHeight = m_quickActionItem->implicitHeight(); + } + implicitHeight += std::max({avatarHeight, contentHeight, quickActionHeight}); if (avatarHeight > 0 || contentHeight > 0) { numObj++; } @@ -528,6 +581,17 @@ void MessageDelegateBase::resizeContent() m_contentItem->setSize(QSizeF(contentItemWidth, m_contentItem->implicitHeight())); yAdd = std::max(yAdd, m_contentItem->implicitHeight()); } + if (m_quickActionItem) { + const auto availableWidth = m_contentItem && showMessageOnRight() ? m_contentItem->x() - m_contentSizeHelper.leftPadding() + : m_sizeHelper.rightX() - m_contentItem->x() - m_contentItem->width() - m_spacing; + m_quickActionItem->setProperty("availableWidth", availableWidth); + const auto actionX = showMessageOnRight() && m_contentItem ? m_contentItem->x() - m_quickActionItem->implicitWidth() - m_spacing + : m_contentItem->x() + m_contentItem->width() + m_spacing; + const auto actionWidth = std::min(m_quickActionItem->implicitWidth(), availableWidth); + m_quickActionItem->setPosition(QPointF(actionX, nextY)); + m_quickActionItem->setSize(QSizeF(actionWidth, m_quickActionItem->implicitHeight())); + yAdd = std::max(yAdd, m_quickActionItem->implicitHeight()); + } nextY += yAdd + m_spacing; if (m_showReadMarkers && m_readMarkerItem) { qreal extraSpacing = m_readMarkerItem->implicitWidth() < m_sizeHelper.availableWidth() - m_spacing ? m_spacing : 0; @@ -545,6 +609,7 @@ void MessageDelegateBase::hoverEnterEvent(QHoverEvent *event) Q_EMIT hoveredChanged(); event->setAccepted(true); updateBackground(); + updateQuickAction(); } void MessageDelegateBase::hoverMoveEvent(QHoverEvent *event) @@ -556,6 +621,7 @@ void MessageDelegateBase::hoverMoveEvent(QHoverEvent *event) } event->setAccepted(true); updateBackground(); + updateQuickAction(); } void MessageDelegateBase::hoverLeaveEvent(QHoverEvent *event) @@ -564,6 +630,7 @@ void MessageDelegateBase::hoverLeaveEvent(QHoverEvent *event) Q_EMIT hoveredChanged(); event->setAccepted(true); updateBackground(); + updateQuickAction(); } bool MessageDelegateBase::isTemporaryHighlighted() const diff --git a/src/timeline/messagedelegate.h b/src/timeline/messagedelegate.h index 50ef2d53c..9fd9b30bc 100644 --- a/src/timeline/messagedelegate.h +++ b/src/timeline/messagedelegate.h @@ -96,6 +96,11 @@ class MessageDelegateBase : public TimelineDelegate Q_PROPERTY(QQmlComponent *compactBackgroundComponent READ compactBackgroundComponent WRITE setCompactBackgroundComponentt NOTIFY compactBackgroundComponentChanged FINAL) + /** + * @brief The component to use to visualize quick actions. + */ + Q_PROPERTY(QQmlComponent *quickActionComponent READ quickActionComponent WRITE setQuickActionComponent NOTIFY quickActionComponentChanged FINAL) + /** * @brief Whether to use the compact mode appearance. */ @@ -152,6 +157,9 @@ public: bool compactMode() const; void setCompactMode(bool compactMode); + QQmlComponent *quickActionComponent() const; + void setQuickActionComponent(QQmlComponent *quickActionComponent); + bool showLocalMessagesOnRight() const; void setShowLocalMessagesOnRight(bool showLocalMessagesOnRight); @@ -172,6 +180,7 @@ Q_SIGNALS: void readMarkerComponentChanged(); void showReadMarkersChanged(); void compactBackgroundComponentChanged(); + void quickActionComponentChanged(); void compactModeChanged(); void showLocalMessagesOnRightChanged(); void isTemporaryHighlightedChanged(); @@ -211,6 +220,10 @@ private: bool m_compactMode = false; void updateBackground(); + QPointer m_quickActionComponent; + bool m_quickActionIncubating = false; + QPointer m_quickActionItem; + bool m_showLocalMessagesOnRight = true; bool m_hovered = false; @@ -245,4 +258,7 @@ private: void resizeContent() override; QPointer m_temporaryHighlightTimer; + +private Q_SLOTS: + void updateQuickAction(); };