Repository: pajlads/DinkPlugin (java)

Issue:
Hide private chat on screenshot frame
Private chat should be hidden when we take the screenshot to not leak PMs.

Options I'd consider:
 - switching chat to `Game` for a single frame and hiding split chat
 - hiding the chat widget for a frame and hiding split chat

Potential pitfalls:
 - split private chat option
 - level up dialog is in the same place the chat normally is, so blacking out the region isn't really an option

Pull request:
feat: optionally hide chat during screenshots
Closes #206
Related: https://github.com/MidgetJake/UniversalDiscordNotifier/issues/36


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c443f5..65c94ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
 ## Unreleased
 
+- Minor: Add advanced setting to hide chat when screenshotting. (#208)
 - Minor: Rescale screenshots to comply with Discord's 8MB size limit. (#200)
 - Minor: Include combat level in skill notifications. (#203)
 - Dev: Update grade wrapper to v8.1 minor version. (#209)
diff --git a/README.md b/README.md
index 3aef1b5..3cf1b9c 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,7 @@ Some notifiers require in-game settings to be configured to send chat messages u
 * Each notifier can send webhook messages to separate "override" urls
 * Screenshots can be individually configured for each notifier
 * Screenshots are compressed if needed to comply with Discord limits
+* The chat box (and private messages above chat) can be hidden from screenshots
 * The plugin can skip notifications if the current player name is on the user-configured RSN ignore list
 * Users can choose whether their webhook messages are sent in Discord's rich embed format or a traditional format
 * The player name in Discord rich embeds can be linked to various tracking services (from HiScores to Wise Old Man)
diff --git a/src/main/java/dinkplugin/DinkPluginConfig.java b/src/main/java/dinkplugin/DinkPluginConfig.java
index c058ecb..8410108 100644
--- a/src/main/java/dinkplugin/DinkPluginConfig.java
+++ b/src/main/java/dinkplugin/DinkPluginConfig.java
@@ -238,6 +238,18 @@ public interface DinkPluginConfig extends Config {
         return PlayerLookupService.OSRS_HISCORE;
     }
 
+    @ConfigItem(
+        keyName = "screenshotHideChat",
+        name = "Hide Chat in Images",
+        description = "Whether to hide the chat box and private messages when capturing screenshots.<br/>" +
+            "Note: visually you may notice the chat box momentarily flicker as it is hidden for the screenshot.",
+        position = 1009,
+        section = advancedSection
+    )
+    default boolean screenshotHideChat() {
+        return false;
+    }
+
     @ConfigItem(
         keyName = "discordWebhook", // do not rename; would break old configs
         name = "Primary Webhook URLs",
diff --git a/src/main/java/dinkplugin/message/DiscordMessageHandler.java b/src/main/java/dinkplugin/message/DiscordMessageHandler.java
index a0f7d6a..052e6d9 100644
--- a/src/main/java/dinkplugin/message/DiscordMessageHandler.java
+++ b/src/main/java/dinkplugin/message/DiscordMessageHandler.java
@@ -9,6 +9,9 @@ import dinkplugin.util.Utils;
 import lombok.NonNull;
 import lombok.extern.slf4j.Slf4j;
 import net.runelite.api.Client;
+import net.runelite.api.widgets.Widget;
+import net.runelite.api.widgets.WidgetInfo;
+import net.runelite.client.callback.ClientThread;
 import net.runelite.client.ui.DrawManager;
 import net.runelite.client.util.ImageUtil;
 import okhttp3.HttpUrl;
@@ -54,15 +57,17 @@ public class DiscordMessageHandler {
     private final OkHttpClient httpClient;
     private final DinkPluginConfig config;
     private final ScheduledExecutorService executor;
+    private final ClientThread clientThread;
 
     @Inject
     @VisibleForTesting
-    public DiscordMessageHandler(Gson gson, Client client, DrawManager drawManager, OkHttpClient httpClient, DinkPluginConfig config, ScheduledExecutorService executor) {
+    public DiscordMessageHandler(Gson gson, Client client, DrawManager drawManager, OkHttpClient httpClient, DinkPluginConfig config, ScheduledExecutorService executor, ClientThread clientThread) {
         this.gson = gson;
         this.client = client;
         this.drawManager = drawManager;
         this.config = config;
         this.executor = executor;
+        this.clientThread = clientThread;
         this.httpClient = httpClient.newBuilder()
             .addInterceptor(chain -> {
                 Request request = chain.request().newBuilder()
@@ -104,6 +109,10 @@ public class DiscordMessageHandler {
             .addFormDataPart("payload_json", gson.toJson(mBody));
 
         if (sendImage) {
+            // optionally hide chat for privacy in screenshot
+            boolean chatHidden = hideWidget(config.screenshotHideChat(), client, WidgetInfo.CHATBOX);
+            boolean whispersHidden = hideWidget(config.screenshotHideChat(), client, WidgetInfo.PRIVATE_CHAT_MESSAGE);
+
             String screenshotFile = mBody.getType().getScreenshot();
             captureScreenshot(drawManager, config.screenshotScale() / 100.0)
                 .thenApply(image -> reqBodyBuilder
@@ -120,6 +129,12 @@ public class DiscordMessageHandler {
                     log.warn("There was an error creating bytes from captured image", e);
                     return reqBodyBuilder;
                 })
+                .thenApply(body -> {
+                    // unhide any widgets we hid
+                    unhideWidget(chatHidden, client, clientThread, WidgetInfo.CHATBOX);
+                    unhideWidget(whispersHidden, client, clientThread, WidgetInfo.PRIVATE_CHAT_MESSAGE);
+                    return body;
+                })
                 .thenAcceptAsync(body -> sendToMultiple(urlList, body));
         } else {
             sendToMultiple(urlList, reqBodyBuilder);
@@ -249,4 +264,27 @@ public class DiscordMessageHandler {
         );
         return body.withEmbeds(embeds);
     }
+
+    private static boolean hideWidget(boolean shouldHide, Client client, WidgetInfo info) {
+        if (!shouldHide)
+            return false;
+
+        Widget widget = client.getWidget(info);
+        if (widget == null || widget.isHidden())
+            return false;
+
+        widget.setHidden(true);
+        return true;
+    }
+
+    private static void unhideWidget(boolean shouldUnhide, Client client, ClientThread clientThread, WidgetInfo info) {
+        if (!shouldUnhide)
+            return;
+
+        clientThread.invoke(() -> {
+            Widget widget = client.getWidget(info);
+            if (widget != null)
+                widget.setHidden(false);
+        });
+    }
 }


Test patch:
diff --git a/src/test/java/dinkplugin/notifiers/MockedNotifierTest.java b/src/test/java/dinkplugin/notifiers/MockedNotifierTest.java
index 5bf460a..5d19d1f 100644
--- a/src/test/java/dinkplugin/notifiers/MockedNotifierTest.java
+++ b/src/test/java/dinkplugin/notifiers/MockedNotifierTest.java
@@ -83,7 +83,7 @@ abstract class MockedNotifierTest extends MockedTestBase {
     protected SettingsManager settingsManager = Mockito.spy(new SettingsManager(gson, client, clientThread, plugin, config, configManager));
 
     @Bind
-    protected DiscordMessageHandler messageHandler = Mockito.spy(new DiscordMessageHandler(gson, client, drawManager, httpClient, config, executor));
+    protected DiscordMessageHandler messageHandler = Mockito.spy(new DiscordMessageHandler(gson, client, drawManager, httpClient, config, executor, clientThread));
 
     @Override
     protected void setUp() {