Code Cleanup
This commit is contained in:
parent
7214aa1bde
commit
5603f466b3
5 changed files with 144 additions and 167 deletions
|
@ -40,6 +40,7 @@ public class Constants
|
||||||
public static final String libsDirectory = getBCVDirectory() + fs + "libs" + fs;
|
public static final String libsDirectory = getBCVDirectory() + fs + "libs" + fs;
|
||||||
public static String krakatauWorkingDirectory = getBCVDirectory() + fs + "krakatau_" + krakatauVersion;
|
public static String krakatauWorkingDirectory = getBCVDirectory() + fs + "krakatau_" + krakatauVersion;
|
||||||
public static String enjarifyWorkingDirectory = getBCVDirectory() + fs + "enjarify_" + enjarifyVersion;
|
public static String enjarifyWorkingDirectory = getBCVDirectory() + fs + "enjarify_" + enjarifyVersion;
|
||||||
|
public static final String[] SUPPORTED_FILE_EXTENSIONS = new String[]{"jar", "zip", "class", "apk", "dex", "war", "jsp"};
|
||||||
|
|
||||||
public static List<String> recentPlugins;
|
public static List<String> recentPlugins;
|
||||||
public static List<String> recentFiles;
|
public static List<String> recentFiles;
|
||||||
|
|
|
@ -10,11 +10,9 @@ import javax.swing.*;
|
||||||
import javax.swing.filechooser.FileFilter;
|
import javax.swing.filechooser.FileFilter;
|
||||||
|
|
||||||
import org.objectweb.asm.tree.ClassNode;
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
import the.bytecode.club.bytecodeviewer.BytecodeViewer;
|
import the.bytecode.club.bytecodeviewer.*;
|
||||||
import the.bytecode.club.bytecodeviewer.Configuration;
|
|
||||||
import the.bytecode.club.bytecodeviewer.Resources;
|
|
||||||
import the.bytecode.club.bytecodeviewer.Settings;
|
|
||||||
import the.bytecode.club.bytecodeviewer.api.ExceptionUI;
|
import the.bytecode.club.bytecodeviewer.api.ExceptionUI;
|
||||||
|
import the.bytecode.club.bytecodeviewer.gui.components.FileChooser;
|
||||||
import the.bytecode.club.bytecodeviewer.gui.components.VisibleComponent;
|
import the.bytecode.club.bytecodeviewer.gui.components.VisibleComponent;
|
||||||
import the.bytecode.club.bytecodeviewer.gui.components.AboutWindow;
|
import the.bytecode.club.bytecodeviewer.gui.components.AboutWindow;
|
||||||
import the.bytecode.club.bytecodeviewer.gui.components.RunOptions;
|
import the.bytecode.club.bytecodeviewer.gui.components.RunOptions;
|
||||||
|
@ -740,52 +738,20 @@ public class MainViewerGUI extends JFrame {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void selectFile()
|
public void compileOnNewThread()
|
||||||
{
|
{
|
||||||
final JFileChooser fc = new JFileChooser();
|
Thread t = new Thread(() -> BytecodeViewer.compile(true));
|
||||||
|
t.start();
|
||||||
try {
|
|
||||||
File f = new File(Configuration.lastDirectory);
|
|
||||||
if (f.exists())
|
|
||||||
fc.setSelectedFile(f);
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fc.setDialogTitle("Select File or Folder to open in BCV");
|
public void runResources()
|
||||||
fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
|
{
|
||||||
fc.setAcceptAllFileFilterUsed(true);
|
if (BytecodeViewer.getLoadedClasses().isEmpty()) {
|
||||||
fc.setFileFilter(new FileFilter() {
|
BytecodeViewer.showMessage("First open a class, jar, zip, apk or dex file.");
|
||||||
@Override
|
return;
|
||||||
public boolean accept(File f) {
|
|
||||||
if (f.isDirectory())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
String extension = MiscUtils.extension(f.getAbsolutePath());
|
|
||||||
return extension.equals("jar") || extension.equals("zip")
|
|
||||||
|| extension.equals("class") || extension.equals("apk")
|
|
||||||
|| extension.equals("dex") || extension.equals("war") || extension.equals("jsp");
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
new RunOptions().setVisible(true);
|
||||||
public String getDescription() {
|
|
||||||
return "APKs, DEX, Class Files or Zip/Jar/War Archives";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
|
||||||
|
|
||||||
if (returnVal == JFileChooser.APPROVE_OPTION) {
|
|
||||||
Configuration.lastDirectory = fc.getSelectedFile().getAbsolutePath();
|
|
||||||
try {
|
|
||||||
BytecodeViewer.viewer.updateBusyStatus(true);
|
|
||||||
BytecodeViewer.openFiles(new File[]{fc.getSelectedFile()}, true);
|
|
||||||
BytecodeViewer.viewer.updateBusyStatus(false);
|
|
||||||
} catch (Exception e1) {
|
|
||||||
new ExceptionUI(e1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reloadResources()
|
public void reloadResources()
|
||||||
|
@ -826,48 +792,34 @@ public class MainViewerGUI extends JFrame {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void compileOnNewThread()
|
public void selectFile()
|
||||||
{
|
{
|
||||||
Thread t = new Thread(() -> BytecodeViewer.compile(true));
|
final JFileChooser fc = new FileChooser(new File(Configuration.lastDirectory),
|
||||||
t.start();
|
"Select File or Folder to open in BCV",
|
||||||
}
|
"APKs, DEX, Class Files or Zip/Jar/War Archives",
|
||||||
|
Constants.SUPPORTED_FILE_EXTENSIONS);
|
||||||
|
|
||||||
public void runResources()
|
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
||||||
|
if (returnVal == JFileChooser.APPROVE_OPTION)
|
||||||
{
|
{
|
||||||
if (BytecodeViewer.getLoadedClasses().isEmpty()) {
|
Configuration.lastDirectory = fc.getSelectedFile().getAbsolutePath();
|
||||||
BytecodeViewer.showMessage("First open a class, jar, zip, apk or dex file.");
|
try {
|
||||||
return;
|
BytecodeViewer.viewer.updateBusyStatus(true);
|
||||||
|
BytecodeViewer.openFiles(new File[]{fc.getSelectedFile()}, true);
|
||||||
|
BytecodeViewer.viewer.updateBusyStatus(false);
|
||||||
|
} catch (Exception e1) {
|
||||||
|
new ExceptionUI(e1);
|
||||||
}
|
}
|
||||||
|
|
||||||
new RunOptions().setVisible(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showForeignLibraryWarning()
|
|
||||||
{
|
|
||||||
if (!deleteForeignOutdatedLibs.isSelected()) {
|
|
||||||
BytecodeViewer.showMessage("WARNING: With this being toggled off outdated libraries will NOT be "
|
|
||||||
+ "removed. It's also a security issue. ONLY TURN IT OFF IF YOU KNOW WHAT YOU'RE DOING.");
|
|
||||||
}
|
|
||||||
Configuration.deleteForeignLibraries = deleteForeignOutdatedLibs.isSelected();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void selectPythonC() {
|
public void selectPythonC() {
|
||||||
JFileChooser fc = new JFileChooser();
|
final JFileChooser fc = new FileChooser(new File(Configuration.lastDirectory),
|
||||||
fc.setFileFilter(new FileFilter() {
|
"Select Python 2.7 Executable",
|
||||||
@Override
|
"Python (Or PyPy for speed) 2.7 Executable",
|
||||||
public boolean accept(File f) {
|
"everything");
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Python (Or PyPy for speed) 2.7 Executable";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fc.setFileHidingEnabled(false);
|
|
||||||
fc.setAcceptAllFileFilterUsed(false);
|
|
||||||
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
||||||
|
|
||||||
if (returnVal == JFileChooser.APPROVE_OPTION)
|
if (returnVal == JFileChooser.APPROVE_OPTION)
|
||||||
try {
|
try {
|
||||||
Configuration.python = fc.getSelectedFile().getAbsolutePath();
|
Configuration.python = fc.getSelectedFile().getAbsolutePath();
|
||||||
|
@ -877,22 +829,12 @@ public class MainViewerGUI extends JFrame {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void selectJavac() {
|
public void selectJavac() {
|
||||||
JFileChooser fc = new JFileChooser();
|
final JFileChooser fc = new FileChooser(new File(Configuration.lastDirectory),
|
||||||
fc.setFileFilter(new FileFilter() {
|
"Select Javac Executable",
|
||||||
@Override
|
"Javac Executable (Requires JDK 'C:/programfiles/Java/JDK_xx/bin/javac.exe)",
|
||||||
public boolean accept(File f) {
|
"everything");
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Javac Executable (Requires JDK 'C:/programfiles/Java/JDK_xx/bin/javac.exe)";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fc.setFileHidingEnabled(false);
|
|
||||||
fc.setAcceptAllFileFilterUsed(false);
|
|
||||||
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
||||||
|
|
||||||
if (returnVal == JFileChooser.APPROVE_OPTION)
|
if (returnVal == JFileChooser.APPROVE_OPTION)
|
||||||
try {
|
try {
|
||||||
Configuration.javac = fc.getSelectedFile().getAbsolutePath();
|
Configuration.javac = fc.getSelectedFile().getAbsolutePath();
|
||||||
|
@ -902,22 +844,12 @@ public class MainViewerGUI extends JFrame {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void selectJava() {
|
public void selectJava() {
|
||||||
JFileChooser fc = new JFileChooser();
|
final JFileChooser fc = new FileChooser(new File(Configuration.lastDirectory),
|
||||||
fc.setFileFilter(new FileFilter() {
|
"Select Java Executable",
|
||||||
@Override
|
"Java Executable (Inside Of JRE/JDK 'C:/programfiles/Java/JDK_xx/bin/java.exe')",
|
||||||
public boolean accept(File f) {
|
"everything");
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Java Executable (Inside Of JRE/JDK 'C:/programfiles/Java/JDK_xx/bin/java.exe')";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fc.setFileHidingEnabled(false);
|
|
||||||
fc.setAcceptAllFileFilterUsed(false);
|
|
||||||
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
||||||
|
|
||||||
if (returnVal == JFileChooser.APPROVE_OPTION)
|
if (returnVal == JFileChooser.APPROVE_OPTION)
|
||||||
try {
|
try {
|
||||||
Configuration.java = fc.getSelectedFile().getAbsolutePath();
|
Configuration.java = fc.getSelectedFile().getAbsolutePath();
|
||||||
|
@ -927,22 +859,12 @@ public class MainViewerGUI extends JFrame {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void selectPythonC3() {
|
public void selectPythonC3() {
|
||||||
JFileChooser fc = new JFileChooser();
|
final JFileChooser fc = new FileChooser(new File(Configuration.lastDirectory),
|
||||||
fc.setFileFilter(new FileFilter() {
|
"Select Python 3.x Executable",
|
||||||
@Override
|
"Python (Or PyPy for speed) 3.x Executable",
|
||||||
public boolean accept(File f) {
|
"everything");
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Python (Or PyPy for speed) 3.x Executable";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fc.setFileHidingEnabled(false);
|
|
||||||
fc.setAcceptAllFileFilterUsed(false);
|
|
||||||
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
||||||
|
|
||||||
if (returnVal == JFileChooser.APPROVE_OPTION)
|
if (returnVal == JFileChooser.APPROVE_OPTION)
|
||||||
try {
|
try {
|
||||||
Configuration.python3 = fc.getSelectedFile().getAbsolutePath();
|
Configuration.python3 = fc.getSelectedFile().getAbsolutePath();
|
||||||
|
@ -952,23 +874,16 @@ public class MainViewerGUI extends JFrame {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void selectOpenalLibraryFolder() {
|
public void selectOpenalLibraryFolder() {
|
||||||
JFileChooser fc = new JFileChooser();
|
final JFileChooser fc = new FileChooser(new File(Configuration.lastDirectory),
|
||||||
fc.setFileFilter(new FileFilter() {
|
"Select Library Folder",
|
||||||
@Override
|
"Optional Library Folder",
|
||||||
public boolean accept(File f) {
|
"everything");
|
||||||
return f.isDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "Optional Library Folder";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
|
fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
|
||||||
fc.setFileHidingEnabled(false);
|
fc.setFileHidingEnabled(false);
|
||||||
fc.setAcceptAllFileFilterUsed(false);
|
fc.setAcceptAllFileFilterUsed(false);
|
||||||
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
|
||||||
|
|
||||||
|
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
||||||
if (returnVal == JFileChooser.APPROVE_OPTION)
|
if (returnVal == JFileChooser.APPROVE_OPTION)
|
||||||
try {
|
try {
|
||||||
Configuration.library = fc.getSelectedFile().getAbsolutePath();
|
Configuration.library = fc.getSelectedFile().getAbsolutePath();
|
||||||
|
@ -978,22 +893,12 @@ public class MainViewerGUI extends JFrame {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void selectJRERTLibrary() {
|
public void selectJRERTLibrary() {
|
||||||
JFileChooser fc = new JFileChooser();
|
final JFileChooser fc = new FileChooser(new File(Configuration.lastDirectory),
|
||||||
fc.setFileFilter(new FileFilter() {
|
"Select JRE RT Jar",
|
||||||
@Override
|
"JRE RT Library",
|
||||||
public boolean accept(File f) {
|
"everything");
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getDescription() {
|
|
||||||
return "JRE RT Library";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
fc.setFileHidingEnabled(false);
|
|
||||||
fc.setAcceptAllFileFilterUsed(false);
|
|
||||||
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
||||||
|
|
||||||
if (returnVal == JFileChooser.APPROVE_OPTION)
|
if (returnVal == JFileChooser.APPROVE_OPTION)
|
||||||
try {
|
try {
|
||||||
Configuration.rt = fc.getSelectedFile().getAbsolutePath();
|
Configuration.rt = fc.getSelectedFile().getAbsolutePath();
|
||||||
|
@ -1004,12 +909,13 @@ public class MainViewerGUI extends JFrame {
|
||||||
|
|
||||||
public void openExternalPlugin()
|
public void openExternalPlugin()
|
||||||
{
|
{
|
||||||
JFileChooser fc = new JFileChooser();
|
JFileChooser fc = new FileChooser(new File(Configuration.lastDirectory),
|
||||||
|
"Select External Plugin",
|
||||||
|
"External Plugin",
|
||||||
|
"everything");
|
||||||
fc.setFileFilter(PluginManager.fileFilter());
|
fc.setFileFilter(PluginManager.fileFilter());
|
||||||
fc.setFileHidingEnabled(false);
|
|
||||||
fc.setAcceptAllFileFilterUsed(false);
|
|
||||||
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
|
||||||
|
|
||||||
|
int returnVal = fc.showOpenDialog(BytecodeViewer.viewer);
|
||||||
if (returnVal == JFileChooser.APPROVE_OPTION)
|
if (returnVal == JFileChooser.APPROVE_OPTION)
|
||||||
try {
|
try {
|
||||||
BytecodeViewer.viewer.updateBusyStatus(true);
|
BytecodeViewer.viewer.updateBusyStatus(true);
|
||||||
|
@ -1022,12 +928,12 @@ public class MainViewerGUI extends JFrame {
|
||||||
|
|
||||||
public void askBeforeExiting()
|
public void askBeforeExiting()
|
||||||
{
|
{
|
||||||
JOptionPane pane = new JOptionPane(
|
JOptionPane pane = new JOptionPane("Are you sure you want to exit?");
|
||||||
"Are you sure you want to exit?");
|
|
||||||
Object[] options = new String[]{"Yes", "No"};
|
Object[] options = new String[]{"Yes", "No"};
|
||||||
|
|
||||||
pane.setOptions(options);
|
pane.setOptions(options);
|
||||||
JDialog dialog = pane.createDialog(BytecodeViewer.viewer,
|
JDialog dialog = pane.createDialog(BytecodeViewer.viewer, "Bytecode Viewer - Exit");
|
||||||
"Bytecode Viewer - Exit");
|
|
||||||
dialog.setVisible(true);
|
dialog.setVisible(true);
|
||||||
Object obj = pane.getValue();
|
Object obj = pane.getValue();
|
||||||
int result = -1;
|
int result = -1;
|
||||||
|
@ -1035,9 +941,21 @@ public class MainViewerGUI extends JFrame {
|
||||||
if (options[k].equals(obj))
|
if (options[k].equals(obj))
|
||||||
result = k;
|
result = k;
|
||||||
|
|
||||||
if (result == 0) {
|
if (result == 0)
|
||||||
|
{
|
||||||
Configuration.canExit = true;
|
Configuration.canExit = true;
|
||||||
System.exit(0);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void showForeignLibraryWarning()
|
||||||
|
{
|
||||||
|
if (!deleteForeignOutdatedLibs.isSelected())
|
||||||
|
{
|
||||||
|
BytecodeViewer.showMessage("WARNING: With this being toggled off outdated libraries will NOT be "
|
||||||
|
+ "removed. It's also a security issue. ONLY TURN IT OFF IF YOU KNOW WHAT YOU'RE DOING.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Configuration.deleteForeignLibraries = deleteForeignOutdatedLibs.isSelected();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package the.bytecode.club.bytecodeviewer.gui.components;
|
||||||
|
|
||||||
|
import the.bytecode.club.bytecodeviewer.Configuration;
|
||||||
|
import the.bytecode.club.bytecodeviewer.util.MiscUtils;
|
||||||
|
|
||||||
|
import javax.swing.*;
|
||||||
|
import javax.swing.filechooser.FileFilter;
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.HashSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Konloch
|
||||||
|
* @since 6/25/2021
|
||||||
|
*/
|
||||||
|
public class FileChooser extends JFileChooser
|
||||||
|
{
|
||||||
|
private final File filePath;
|
||||||
|
private final String title;
|
||||||
|
private final String description;
|
||||||
|
private final String[] extensions;
|
||||||
|
private final HashSet<String> extensionSet = new HashSet<>();
|
||||||
|
|
||||||
|
public FileChooser(File filePath, String title, String description, String... extensions)
|
||||||
|
{
|
||||||
|
this.filePath = filePath;
|
||||||
|
this.title = title;
|
||||||
|
this.description = description;
|
||||||
|
this.extensions = extensions;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (filePath.exists())
|
||||||
|
setSelectedFile(filePath);
|
||||||
|
} catch (Exception ignored) { }
|
||||||
|
|
||||||
|
setDialogTitle(title);
|
||||||
|
setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
|
||||||
|
setFileHidingEnabled(false);
|
||||||
|
setAcceptAllFileFilterUsed(false);
|
||||||
|
setFileFilter(new FileFilter()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public boolean accept(File f)
|
||||||
|
{
|
||||||
|
if (f.isDirectory())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if(extensions[0].equals("everything"))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return extensionSet.contains(MiscUtils.extension(f.getAbsolutePath()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,7 +30,7 @@ import the.bytecode.club.bytecodeviewer.util.MiscUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supports loading of groovy, python or ruby scripts.
|
* Supports loading of groovy, python or ruby scripts.
|
||||||
* <p>
|
*
|
||||||
* Only allows one plugin to be running at once.
|
* Only allows one plugin to be running at once.
|
||||||
*
|
*
|
||||||
* @author Konloch
|
* @author Konloch
|
||||||
|
|
|
@ -25,9 +25,8 @@ import org.jetbrains.annotations.NotNull;
|
||||||
***************************************************************************/
|
***************************************************************************/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the various newline conventions to the local platform's
|
* Convert the various newline conventions to the local platform's newline convention.
|
||||||
* newline convention. <p>
|
*
|
||||||
* <p>
|
|
||||||
* This stream can be used with the Message.writeTo method to
|
* This stream can be used with the Message.writeTo method to
|
||||||
* generate a message that uses the local plaform's line terminator
|
* generate a message that uses the local plaform's line terminator
|
||||||
* for the purpose of (e.g.) saving the message to a local file.
|
* for the purpose of (e.g.) saving the message to a local file.
|
||||||
|
|
Loading…
Reference in a new issue