diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5788e1b7..bca78d16 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,7 @@ All notable changes to this project will be documented in this file.
- Relay Chat room keys so hosts can require a key to join a room
- Desktop privacy: Windows screen security to omit MeshChatX from screenshots, recording, and Recall
- Android privacy options to block screenshots and clear the clipboard when backgrounded
+- Messages: **Copy image to clipboard** on the message and full-screen image context menus
- Remote management allow-list for identities that may query this instance with rnstatus/rnpath
- Post-install prompts for existing users after upgrades
- Coolify-oriented Docker Compose with resource limits for deployments
@@ -56,6 +57,7 @@ All notable changes to this project will be documented in this file.
### Fixed
+- Desktop AppImage: closed stdout no longer raises a main-process **write EPIPE** dialog when MeshChatX is backgrounded. Logging uses broken-pipe guards
- Android: lxmfy packaging, flock soft-lock, splash/logo clipping, Landlock skipped on Android
- Android RNode BLE/USB via Chaquopy
- Startup check and disable unsupported interfaces
diff --git a/electron/main.js b/electron/main.js
index a1215829..3b1b24f0 100644
--- a/electron/main.js
+++ b/electron/main.js
@@ -42,6 +42,9 @@ const {
applyContentProtection,
applyContentProtectionToWindows,
} = require("./desktopPrivacySettings");
+const { installBrokenPipeGuards, safeConsoleLog } = require("./safeConsole");
+
+installBrokenPipeGuards(process);
// remember main window
var mainWindow = null;
@@ -84,7 +87,7 @@ try {
const disableGpuFile = path.join(storageDir, "disable-gpu");
if (fs.existsSync(disableGpuFile)) {
app.disableHardwareAcceleration();
- console.log("Hardware acceleration disabled via storage flag.");
+ safeConsoleLog("Hardware acceleration disabled via storage flag.");
}
} catch {
// ignore errors reading storage dir this early
@@ -102,7 +105,7 @@ if (process.platform === "linux") {
// Detect if running in Flatpak sandbox
const isRunningInFlatpak = !!process.env.FLATPAK_ID;
if (isRunningInFlatpak) {
- console.log(`Running in Flatpak sandbox: ${process.env.FLATPAK_ID}`);
+ safeConsoleLog(`Running in Flatpak sandbox: ${process.env.FLATPAK_ID}`);
}
// Protocol registration
@@ -629,6 +632,26 @@ function attachDefaultContextMenu(browserWindow) {
});
}
+ if (params.mediaType === "image" || params.hasImageContents) {
+ if (template.length > 0) {
+ template.push({ type: "separator" });
+ }
+ template.push({
+ label: "Copy image",
+ click: () => {
+ webContents.copyImageAt(params.x, params.y);
+ },
+ });
+ if (params.srcURL) {
+ template.push({
+ label: "Copy image address",
+ click: () => {
+ clipboard.writeText(params.srcURL);
+ },
+ });
+ }
+ }
+
if (template.length === 0) {
return;
}
@@ -639,8 +662,8 @@ function attachDefaultContextMenu(browserWindow) {
}
function log(message) {
- // log to stdout of this process
- console.log(message);
+ // log to stdout of this process (AppImage may close the pipe later)
+ safeConsoleLog(message);
// make sure main window exists
if (!mainWindow) {
diff --git a/electron/safeConsole.js b/electron/safeConsole.js
new file mode 100644
index 00000000..68c6512e
--- /dev/null
+++ b/electron/safeConsole.js
@@ -0,0 +1,61 @@
+// SPDX-License-Identifier: 0BSD
+
+/**
+ * Broken-pipe (EPIPE) guards for Electron AppImage / launcher sessions where
+ * stdout or stderr is closed while the main process still logs.
+ */
+
+function isBrokenPipeError(err) {
+ if (!err || typeof err !== "object") {
+ return false;
+ }
+ if (err.code === "EPIPE" || err.errno === "EPIPE") {
+ return true;
+ }
+ const message = typeof err.message === "string" ? err.message : "";
+ return /\bEPIPE\b/i.test(message);
+}
+
+/**
+ * Attach no-op error listeners so closed stdout/stderr do not become
+ * uncaughtException dialogs when console.log writes after the pipe closes.
+ * @param {{ stdout?: NodeJS.WritableStream, stderr?: NodeJS.WritableStream } | null | undefined} [proc]
+ */
+function installBrokenPipeGuards(proc = process) {
+ const attach = (stream) => {
+ if (!stream || typeof stream.on !== "function") {
+ return;
+ }
+ if (stream.__meshchatxBrokenPipeGuarded) {
+ return;
+ }
+ stream.__meshchatxBrokenPipeGuarded = true;
+ stream.on("error", (err) => {
+ if (isBrokenPipeError(err)) {
+ return;
+ }
+ });
+ };
+ attach(proc?.stdout);
+ attach(proc?.stderr);
+}
+
+/**
+ * console.log that never throws on a closed stdout pipe.
+ * @param {...unknown} args
+ */
+function safeConsoleLog(...args) {
+ try {
+ console.log(...args);
+ } catch (err) {
+ if (!isBrokenPipeError(err)) {
+ throw err;
+ }
+ }
+}
+
+module.exports = {
+ isBrokenPipeError,
+ installBrokenPipeGuards,
+ safeConsoleLog,
+};
diff --git a/meshchatx.rsm b/meshchatx.rsm
index 495bdf4a..dbefb9ed 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ
diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
index 715053c8..adead7e3 100644
--- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
@@ -968,6 +968,13 @@
{{ $t("messages.save_image_to_device") }}
+
+
+ {{ $t("messages.copy_image_to_clipboard") }}
+
{{ $t("messages.save_image_to_device") }}
+
+
+ {{ $t("messages.copy_image_to_clipboard") }}
+
@@ -1646,7 +1657,7 @@