diff --git a/.github/workflows/macos-build-and-test.yml b/.github/workflows/macos-build-and-test.yml
index 2005faef02bba..e1ed01a20e01f 100644
--- a/.github/workflows/macos-build-and-test.yml
+++ b/.github/workflows/macos-build-and-test.yml
@@ -78,6 +78,10 @@ jobs:
timeout-minutes: 60
runs-on: macos-26
steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ with:
+ fetch-depth: 1
+
- name: Restore cached Craft directories containing the built client
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
diff --git a/resources.qrc b/resources.qrc
index a9e58cda6e896..cb4cc6218d7ff 100644
--- a/resources.qrc
+++ b/resources.qrc
@@ -40,6 +40,8 @@
src/gui/tray/TrayWindowHeader.qml
src/gui/activity/qml/ActivityItemContextMenu.qml
src/gui/activity/qml/ActivityItemActions.qml
+ src/gui/activity/qml/ActivityFileMenu.qml
+ src/gui/activity/qml/ActivityFileMenuButton.qml
src/gui/activity/qml/ActivityItemContent.qml
src/gui/activity/qml/TalkReplyTextField.qml
src/gui/tray/CallNotificationDialog.qml
diff --git a/src/gui/activity/qml/ActivityFileMenu.qml b/src/gui/activity/qml/ActivityFileMenu.qml
new file mode 100644
index 0000000000000..465648d92e05b
--- /dev/null
+++ b/src/gui/activity/qml/ActivityFileMenu.qml
@@ -0,0 +1,46 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Controls
+import "../../tray"
+
+Menu {
+ id: root
+
+ required property string filePath
+ required property bool serverHasIntegration
+ required property int itemFontPixelSize
+
+ signal fileDetailsRequested(string filePath)
+ signal fileActionsRequested(string filePath)
+
+ closePolicy: Menu.CloseOnPressOutsideParent | Menu.CloseOnEscape
+
+ property Action fileDetailsAction: Action {
+ id: fileDetailsAction
+ objectName: "fileDetailsAction"
+ text: qsTr("File details")
+ onTriggered: root.fileDetailsRequested(root.filePath)
+ }
+
+ property Action fileActionsAction: Action {
+ id: fileActionsAction
+ objectName: "fileActionsAction"
+ text: qsTr("File actions")
+ onTriggered: root.fileActionsRequested(root.filePath)
+ }
+
+ MenuItem {
+ id: fileDetailsMenuItem
+ action: root.fileDetailsAction
+ }
+
+ MenuItem {
+ id: fileActionsMenuItem
+ action: root.fileActionsAction
+ visible: root.serverHasIntegration
+ }
+}
diff --git a/src/gui/activity/qml/ActivityFileMenuButton.qml b/src/gui/activity/qml/ActivityFileMenuButton.qml
new file mode 100644
index 0000000000000..5e6d16afd1fef
--- /dev/null
+++ b/src/gui/activity/qml/ActivityFileMenuButton.qml
@@ -0,0 +1,51 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import QtQuick
+import QtQuick.Controls
+
+Button {
+ id: root
+
+ required property string filePath
+ required property bool serverHasIntegration
+ required property int itemFontPixelSize
+ required property int buttonWidth
+ required property int buttonHeight
+ required property int buttonIconSize
+
+ readonly property alias menu: fileMenu
+
+ signal fileDetailsRequested(string filePath)
+ signal fileActionsRequested(string filePath)
+
+ width: buttonWidth
+ height: buttonHeight
+
+ icon.name: "view-more-symbolic"
+ icon.source: "image://svgimage-custom-color/more.svg/" + palette.buttonText
+ icon.width: buttonIconSize
+ icon.height: buttonIconSize
+
+ ToolTip {
+ popupType: Qt.platform.os === "windows" ? Popup.Item : Popup.Native
+ text: qsTr("Open file details")
+ visible: parent.hovered
+ }
+
+ display: Button.IconOnly
+ onClicked: fileMenu.visible ? fileMenu.close() : fileMenu.popup()
+
+ ActivityFileMenu {
+ id: fileMenu
+
+ filePath: root.filePath
+ serverHasIntegration: root.serverHasIntegration
+ itemFontPixelSize: root.itemFontPixelSize
+
+ onFileDetailsRequested: path => root.fileDetailsRequested(path)
+ onFileActionsRequested: path => root.fileActionsRequested(path)
+ }
+}
diff --git a/src/gui/activity/qml/ActivityItemContent.qml b/src/gui/activity/qml/ActivityItemContent.qml
index 822259d97d734..253c164e7c522 100644
--- a/src/gui/activity/qml/ActivityItemContent.qml
+++ b/src/gui/activity/qml/ActivityItemContent.qml
@@ -170,48 +170,19 @@ RowLayout {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
spacing: Style.extraSmallSpacing
- Button {
+ ActivityFileMenuButton {
id: fileDetailsButton
- width: Style.activityListButtonWidth
- height: Style.activityListButtonHeight
-
- icon.name: 'view-more-symbolic'
- icon.source: "image://svgimage-custom-color/more.svg/" + palette.buttonText
- icon.width: Style.activityListButtonIconSize
- icon.height: Style.activityListButtonIconSize
-
- ToolTip {
- popupType: Qt.platform.os === "windows" ? Popup.Item : Qt.platform.os === "windows" ? Popup.Item : Popup.Native
- text: qsTr("Open file details")
- visible: parent.hovered
- }
-
- display: Button.IconOnly
+ buttonWidth: Style.activityListButtonWidth
+ buttonHeight: Style.activityListButtonHeight
+ buttonIconSize: Style.activityListButtonIconSize
+ itemFontPixelSize: Style.topLinePixelSize
+ filePath: root.activityData.openablePath
+ serverHasIntegration: root.activityData.serverHasIntegration
visible: model.showFileDetails
- onClicked: fileMoreButtonMenu.visible ? fileMoreButtonMenu.close() : fileMoreButtonMenu.popup()
-
- AutoSizingMenu {
- id: fileMoreButtonMenu
- closePolicy: Menu.CloseOnPressOutsideParent | Menu.CloseOnEscape
-
- MenuItem {
- height: visible ? implicitHeight : 0
- text: qsTr("File details")
- font.pixelSize: Style.topLinePixelSize
- hoverEnabled: true
- onClicked: Systray.presentShareViewInTray(model.openablePath)
- }
-
- MenuItem {
- visible: model.serverHasIntegration
- height: visible ? implicitHeight : 0
- text: qsTr("File actions")
- font.pixelSize: Style.topLinePixelSize
- hoverEnabled: true
- onClicked: Systray.presentFileActionsViewInSystray(model.openablePath)
- }
- }
+
+ onFileDetailsRequested: path => Systray.presentShareViewInTray(path)
+ onFileActionsRequested: path => Systray.presentFileActionsViewInSystray(path)
}
Button {
diff --git a/src/gui/tray/AutoSizingMenu.qml b/src/gui/tray/AutoSizingMenu.qml
index e3cc79871d459..835bcaa1bdd57 100644
--- a/src/gui/tray/AutoSizingMenu.qml
+++ b/src/gui/tray/AutoSizingMenu.qml
@@ -5,7 +5,6 @@
import QtQuick
import QtQuick.Controls
-import Style
Menu {
popupType: Popup.Window
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index cfa3a89a95d0e..6092b21965af3 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -101,6 +101,10 @@ nextcloud_add_test(UnifiedSearchListmodel)
nextcloud_add_test(ActivityListModel)
nextcloud_add_test(SortedActivityListModel)
nextcloud_add_test(ActivityData)
+add_test(NAME ActivityFileMenuQmlTest
+ COMMAND Qt6::qmltestrunner
+ -input "${CMAKE_CURRENT_SOURCE_DIR}/qml/activityfilemenu/testactivityfilemenu.qml"
+)
nextcloud_add_test(TalkReply)
nextcloud_add_test(LockFile)
nextcloud_add_test(ShareModel)
diff --git a/test/qml/activityfilemenu/testactivityfilemenu.qml b/test/qml/activityfilemenu/testactivityfilemenu.qml
new file mode 100644
index 0000000000000..84dc2db2b2770
--- /dev/null
+++ b/test/qml/activityfilemenu/testactivityfilemenu.qml
@@ -0,0 +1,103 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+import QtQuick
+import QtTest
+import "../../../src/gui/activity/qml"
+
+Item {
+ id: testRoot
+
+ width: 200
+ height: 100
+
+ TestCase {
+ id: testCase
+
+ name: "ActivityFileMenu"
+ when: windowShown
+
+ property ActivityFileMenuButton button
+
+ SignalSpy {
+ id: fileDetailsRequestedSpy
+ signalName: "fileDetailsRequested"
+ }
+
+ SignalSpy {
+ id: fileActionsRequestedSpy
+ signalName: "fileActionsRequested"
+ }
+
+ Component {
+ id: buttonComponent
+
+ ActivityFileMenuButton {
+ filePath: "/sync/folder/file.txt"
+ serverHasIntegration: true
+ itemFontPixelSize: 14
+ buttonWidth: 44
+ buttonHeight: 32
+ buttonIconSize: 16
+ }
+ }
+
+ function init()
+ {
+ button = createTemporaryObject(buttonComponent, testRoot);
+ verify(button);
+
+ fileDetailsRequestedSpy.target = button;
+ fileActionsRequestedSpy.target = button;
+ }
+
+ function test_contentFitsWithoutScrolling()
+ {
+ button.menu.popup();
+ tryCompare(button.menu, "opened", true);
+ compare(button.menu.count, 2);
+ const lastItem = button.menu.itemAt(button.menu.count - 1);
+ verify(lastItem);
+ verify(button.menu.availableHeight >= lastItem.y + lastItem.height);
+ }
+
+ function openMenu()
+ {
+ mouseClick(button);
+ tryCompare(button.menu, "opened", true);
+ }
+
+ function test_buttonOpensMenu()
+ {
+ verify(!button.menu.opened);
+ openMenu();
+ verify(button.menu.opened);
+ }
+
+ function test_fileDetailsItemEmitsCapturedPath()
+ {
+ openMenu();
+
+ const fileDetailsItem = button.menu.itemAt(0);
+ verify(fileDetailsItem);
+ mouseClick(fileDetailsItem);
+
+ compare(fileDetailsRequestedSpy.count, 1);
+ compare(fileDetailsRequestedSpy.signalArguments[0][0], "/sync/folder/file.txt");
+ }
+
+ function test_fileActionsItemEmitsCapturedPath()
+ {
+ openMenu();
+
+ const fileActionsItem = button.menu.itemAt(1);
+ verify(fileActionsItem);
+ mouseClick(fileActionsItem);
+
+ compare(fileActionsRequestedSpy.count, 1);
+ compare(fileActionsRequestedSpy.signalArguments[0][0], "/sync/folder/file.txt");
+ }
+ }
+}