From bc969f6d2b610eac27315fb52a18be389b7f7e38 Mon Sep 17 00:00:00 2001
From: tduva
Date: Fri, 25 Mar 2016 03:09:02 +0100
Subject: [PATCH] Announcements - Add Announcements support - Add
Announcements dialog - Add support for Announcement notification to main
menu bar - Auto request on start and every 6 hours - Change
DateTime.agoText() to support years - Update help - Add debug log directory
function missing from last commit
---
src/chatty/Chatty.java | 4 +
src/chatty/SettingsManager.java | 3 +
src/chatty/TwitchClient.java | 2 +
src/chatty/gui/MainGui.java | 19 +-
src/chatty/gui/MainMenu.java | 138 +++++---
src/chatty/gui/components/NewsDialog.java | 332 ++++++++++++++++++
.../gui/components/help/help-addressbook.html | 24 +-
src/chatty/util/DateTime.java | 10 +-
src/chatty/util/JSONUtil.java | 8 +
src/chatty/util/MiscUtil.java | 8 +
10 files changed, 494 insertions(+), 54 deletions(-)
create mode 100644 src/chatty/gui/components/NewsDialog.java
diff --git a/src/chatty/Chatty.java b/src/chatty/Chatty.java
index 2482dab9b..3d8188f83 100644
--- a/src/chatty/Chatty.java
+++ b/src/chatty/Chatty.java
@@ -225,6 +225,10 @@ public static String getBackupDirectory() {
return getUserDataDirectory()+"backup"+File.separator;
}
+ public static String getDebugLogDirectory() {
+ return getUserDataDirectory()+"debuglogs"+File.separator;
+ }
+
public static String chattyVersion() {
return String.format("Chatty Version %s%s%s / %s",
Chatty.VERSION,
diff --git a/src/chatty/SettingsManager.java b/src/chatty/SettingsManager.java
index a7fc697c6..8d69b55bc 100644
--- a/src/chatty/SettingsManager.java
+++ b/src/chatty/SettingsManager.java
@@ -420,6 +420,9 @@ void defineSettings() {
settings.addString("cmChannel", "");
settings.addString("cmTemplate", "{user}: {message}");
settings.addBoolean("cmHighlightedOnly", false);
+
+ settings.addBoolean("newsAutoRequest", true);
+ settings.addLong("newsLastRead", 0);
}
/**
diff --git a/src/chatty/TwitchClient.java b/src/chatty/TwitchClient.java
index 8265225d3..8a64351aa 100644
--- a/src/chatty/TwitchClient.java
+++ b/src/chatty/TwitchClient.java
@@ -1000,6 +1000,8 @@ private void testCommands(String channel, String command, String parameter) {
testUser.setColor(parameter);
} else if (command.equals("testupdatenotification")) {
g.setUpdateAvailable("[test]");
+ } else if (command.equals("testannouncement")) {
+ g.setAnnouncementAvailable(Boolean.parseBoolean(parameter));
} else if (command.equals("removechan")) {
g.removeChannel(parameter);
} else if (command.equals("testtimer")) {
diff --git a/src/chatty/gui/MainGui.java b/src/chatty/gui/MainGui.java
index c180460de..2db1f7952 100644
--- a/src/chatty/gui/MainGui.java
+++ b/src/chatty/gui/MainGui.java
@@ -35,6 +35,7 @@
import chatty.gui.components.FollowersDialog;
import chatty.gui.components.LiveStreamsDialog;
import chatty.gui.components.LivestreamerDialog;
+import chatty.gui.components.NewsDialog;
import chatty.gui.components.srl.SRL;
import chatty.gui.components.SearchDialog;
import chatty.gui.components.StreamChat;
@@ -118,6 +119,7 @@ public class MainGui extends JFrame implements Runnable {
private SRL srl;
private LivestreamerDialog livestreamerDialog;
private UpdateMessage updateMessage;
+ private NewsDialog newsDialog;
private EmotesDialog emotesDialog;
private FollowersDialog followerDialog;
private FollowersDialog subscribersDialog;
@@ -241,6 +243,7 @@ private void createGui() {
srl = new SRL(this, client.speedrunsLive, contextMenuListener);
livestreamerDialog = new LivestreamerDialog(this, linkLabelListener, client.settings);
updateMessage = new UpdateMessage(this);
+ newsDialog = new NewsDialog(this, client.settings);
client.settings.addSettingChangeListener(new MySettingChangeListener());
client.settings.addSettingsListener(new MySettingsListener());
@@ -254,7 +257,7 @@ private void createGui() {
// Main Menu
MainMenuListener menuListener = new MainMenuListener();
- menu = new MainMenu(menuListener,menuListener);
+ menu = new MainMenu(menuListener,menuListener, linkLabelListener);
setJMenuBar(menu);
state.update();
@@ -669,6 +672,8 @@ public void run() {
// Should be done when the main window is already visible, so
// it can be centered on it correctly, if that is necessary
reopenWindows();
+
+ newsDialog.autoRequestNews(true);
}
});
}
@@ -1170,6 +1175,10 @@ public void linkClicked(String type, String ref) {
if (ref.equals("show")) {
openUpdateDialog();
}
+ } else if (type.equals("announcement")) {
+ if (ref.equals("show")) {
+ newsDialog.showDialog();
+ }
}
}
}
@@ -1229,6 +1238,8 @@ public void actionPerformed(ActionEvent e) {
exit();
} else if (cmd.equals("about")) {
openHelp("");
+ } else if (cmd.equals("news")) {
+ newsDialog.showDialog();
} else if (cmd.equals("settings")) {
getSettingsDialog().showSettings();
} else if (cmd.equals("saveSettings")) {
@@ -2807,12 +2818,16 @@ public void setUpdateAvailable(final String newVersion) {
@Override
public void run() {
- menu.setUpdateAvailable(linkLabelListener);
+ menu.setUpdateNotification(true);
updateMessage.setNewVersion(newVersion);
}
});
}
+ public void setAnnouncementAvailable(boolean enabled) {
+ menu.setAnnouncementNotification(enabled);
+ }
+
public void showSettings() {
SwingUtilities.invokeLater(new Runnable() {
diff --git a/src/chatty/gui/MainMenu.java b/src/chatty/gui/MainMenu.java
index 286f76208..b316985a7 100644
--- a/src/chatty/gui/MainMenu.java
+++ b/src/chatty/gui/MainMenu.java
@@ -43,30 +43,24 @@ public class MainMenu extends JMenuBar {
private final ItemListener itemListener;
private final ActionListener actionListener;
+ private final LinkLabelListener linkLabelListener;
// Set here because it is used more than once
private final String IGNORED_LABEL = "Ignored";
private final String HIGHLIGHTS_LABEL = "Highlights";
- /**
- * Stores whether the "Update Available!" message has been added yet, so
- * it's guaranteed to be only added once.
- */
- private boolean addedUpdateMessage;
- /**
- * Store whether the update notification is currently set to the smaller
- * version, so it doesn't constantly change unless necessary.
- */
- private boolean updateMessageSmaller;
+ private final Notification notification = new Notification();
/**
* Stores all the menu items associated with a key
*/
private final HashMap menuItems = new HashMap<>();
- public MainMenu(ActionListener actionListener, ItemListener itemListener) {
+ public MainMenu(ActionListener actionListener, ItemListener itemListener,
+ LinkLabelListener linkLabelListener) {
this.itemListener = itemListener;
this.actionListener = actionListener;
+ this.linkLabelListener = linkLabelListener;
//this.setBackground(Color.black);
//this.setForeground(Color.white);
@@ -178,6 +172,8 @@ public MainMenu(ActionListener actionListener, ItemListener itemListener) {
JMenuItem helpItem = addItem(help,"about","About/Help", KeyEvent.VK_H);
helpItem.setAccelerator(KeyStroke.getKeyStroke("F1"));
setIcon(helpItem, "help-browser.png");
+ help.addSeparator();
+ addItem(help,"news","Announcements");
add(main);
@@ -332,57 +328,111 @@ public void updateSrlStreams(String active, List popout) {
}
}
- /**
- * Regular version of the update notification.
- */
- private static final String UPDATE_MESSAGE = ""
- + ""
- + "[update:show Update available!]";
- /**
- * Smaller version of the update notification.
- */
- private static final String UPDATE_MESSAGE_SMALL = ""
- + ""
- + "[update:show Update!]";
+ public void setUpdateNotification(boolean enabled) {
+ notification.setUpdateNotification(enabled);
+ }
- /**
- * Add the Update available! link in the menubar.
- *
- * @param listener The listener that reacts on a click on the link
- */
- public void setUpdateAvailable(LinkLabelListener listener) {
- if (!addedUpdateMessage) {
- final LinkLabel updateNotification = new LinkLabel(UPDATE_MESSAGE, listener);
+ public void setAnnouncementNotification(boolean enabled) {
+ notification.setAnnouncementNotification(enabled);
+ }
+
+ private class Notification {
+
+ private static final String MESSAGE_BASE = ""
+ + "";
+
+ /**
+ * Stores whether the notification label has been added to the layout
+ * yet, so it's guaranteed to be only added once.
+ */
+ private boolean addedLabelToLayout;
+
+ /**
+ * Store whether the notification is currently set to the smaller
+ * version, so it doesn't constantly change unless necessary.
+ */
+ private boolean updateMessageSmaller;
+
+ private String message;
+ private String shortMessage;
+ private Dimension preferredSize = new Dimension();
+ private LinkLabel notification;
+ private boolean updateNotificationEnabled;
+ private boolean announcementNotificationEnabled;
+
+ public void setUpdateNotification(boolean enabled) {
+ if (updateNotificationEnabled != enabled) {
+ updateNotificationEnabled = enabled;
+ setNotification();
+ }
+ }
+
+ public void setAnnouncementNotification(boolean enabled) {
+ if (announcementNotificationEnabled != enabled) {
+ announcementNotificationEnabled = enabled;
+ setNotification();
+ }
+ }
+
+ private void makeText() {
+ message = MESSAGE_BASE;
+ shortMessage = MESSAGE_BASE;
+ if (announcementNotificationEnabled) {
+ message += "[announcement:show Announcement]";
+ shortMessage += "[announcement:show News]";
+ }
+ if (updateNotificationEnabled) {
+ if (announcementNotificationEnabled) {
+ message += " - ";
+ shortMessage += " - ";
+ }
+ message += "[update:show Update available!]";
+ shortMessage += "[update:show Update!]";
+ }
+ }
+
+ private void setNotification() {
+ makeText();
+ if (!addedLabelToLayout) {
+ addNotificationToLayout();
+ addedLabelToLayout = true;
+ }
- // Add listener and stuff to change notification size when less
- // space is there (Update available! -> Update!)
- // Save the preferred size for the regular version here, because
- // checking the preferred size in the listener would change between
- // the regular and smaller version
- final Dimension requiredSize = updateNotification.getPreferredSize();
- updateNotification.addComponentListener(new ComponentAdapter() {
+ // Save preferred size of regular version to compare to in listener
+ notification.setText(message);
+ preferredSize = notification.getPreferredSize();
+ }
+
+ private void addNotificationToLayout() {
+ notification = new LinkLabel("", linkLabelListener);
+
+ /**
+ * Add listener to change notification text to a shorter version
+ * when less space is available ("Update available!" -> "Update!").
+ */
+ notification.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
Dimension actualSize = e.getComponent().getSize();
- if (actualSize.width < requiredSize.width+10) {
+ if (actualSize.width < preferredSize.width + 10) {
if (!updateMessageSmaller) {
- updateNotification.setText(UPDATE_MESSAGE_SMALL);
+ notification.setText(shortMessage);
updateMessageSmaller = true;
//System.out.println("made smaller");
}
} else {
if (updateMessageSmaller) {
- updateNotification.setText(UPDATE_MESSAGE);
+ notification.setText(message);
updateMessageSmaller = false;
//System.out.println("made bigger again");
}
}
}
});
-
- add(updateNotification);
- addedUpdateMessage = true;
+
+ add(notification);
}
+
}
}
diff --git a/src/chatty/gui/components/NewsDialog.java b/src/chatty/gui/components/NewsDialog.java
new file mode 100644
index 000000000..2cfb09164
--- /dev/null
+++ b/src/chatty/gui/components/NewsDialog.java
@@ -0,0 +1,332 @@
+
+package chatty.gui.components;
+
+import chatty.gui.MainGui;
+import chatty.gui.UrlOpener;
+import chatty.util.DateTime;
+import chatty.util.JSONUtil;
+import chatty.util.MiscUtil;
+import chatty.util.UrlRequest;
+import chatty.util.settings.Settings;
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextPane;
+import javax.swing.SwingUtilities;
+import javax.swing.Timer;
+import javax.swing.event.HyperlinkEvent;
+import javax.swing.event.HyperlinkListener;
+import org.json.simple.JSONArray;
+import org.json.simple.JSONObject;
+import org.json.simple.parser.JSONParser;
+import org.json.simple.parser.ParseException;
+
+/**
+ * Show short announcements that are requested from a JSON file.
+ *
+ * When the "Mark as read" button is pressed, the timestamp of the latest news
+ * is stored to prevent those news from being shown as new.
+ *
+ * @author tduva
+ */
+public class NewsDialog extends JDialog {
+
+ private static final Logger LOGGER = Logger.getLogger(NewsDialog.class.getName());
+
+ private static final String NEWS_URL = "http://chatty.github.io/news.json";
+// private static final String NEWS_URL = "http://127.0.0.1/twitch/news.json";
+
+ private static final String SETTING_LAST_READ_TIMESTAMP = "newsLastRead";
+ private static final int REQUEST_DELAY = 60*1000;
+ private static final int TIMER_DELAY = (int)TimeUnit.HOURS.toMillis(6);
+
+ private final MainGui main;
+ private final Settings settings;
+
+ // State
+ private long lastRequested;
+ private long latestNewsTimestamp;
+ private String cachedNews;
+
+ // GUI
+ private final JLabel version;
+ private final JTextPane news;
+
+ public NewsDialog(MainGui main, Settings settings) {
+ super(main);
+
+ this.main = main;
+ this.settings = settings;
+
+ news = new JTextPane();
+ news.setEditable(false);
+ news.addHyperlinkListener(new HyperlinkListener() {
+
+ @Override
+ public void hyperlinkUpdate(HyperlinkEvent e) {
+ if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
+ String url = e.getURL().toString();
+ String protocol = e.getURL().getProtocol();
+ if (protocol.equals("http") || protocol.equals("https")) {
+ UrlOpener.openUrlPrompt(NewsDialog.this, url, true);
+ }
+ }
+ }
+ });
+ news.setContentType("text/html");
+
+ version = new JLabel();
+
+ add(version, BorderLayout.NORTH);
+ add(new JScrollPane(news), BorderLayout.CENTER);
+
+ JPanel buttons = new JPanel(new GridBagLayout());
+ add(buttons, BorderLayout.SOUTH);
+
+ final JButton markRead = new JButton("Mark as read & Close");
+ final JButton close = new JButton("Close");
+ final JButton refresh = new JButton(new ImageIcon(NewsDialog.class.getResource("view-refresh.png")));
+
+ GridBagConstraints gbc = new GridBagConstraints();
+
+ gbc.gridx = 0;
+ gbc.gridy = 0;
+ gbc.fill = GridBagConstraints.VERTICAL;
+ gbc.anchor = GridBagConstraints.WEST;
+ gbc.weightx = 0.8;
+ gbc.insets = new Insets(5, 4, 5, 4);
+ buttons.add(refresh, gbc);
+
+ gbc.gridx = 1;
+ gbc.gridy = 0;
+ gbc.anchor = GridBagConstraints.EAST;
+ gbc.weightx = 0;
+ gbc.insets = new Insets(5, 4, 5, 4);
+ buttons.add(markRead, gbc);
+
+ gbc.gridx = 2;
+ gbc.gridy = 0;
+ gbc.insets = new Insets(5, 1, 5, 4);
+ buttons.add(close, gbc);
+
+
+ ActionListener buttonAction = new ActionListener() {
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if (e.getSource() == markRead) {
+ if (latestNewsTimestamp > 0) {
+ settings.setLong(SETTING_LAST_READ_TIMESTAMP, latestNewsTimestamp);
+ setNews(cachedNews);
+ }
+ setVisible(false);
+ } else if (e.getSource() == close) {
+ setVisible(false);
+ }
+ else if (e.getSource() == refresh) {
+ requestNews(false);
+ }
+ }
+ };
+ refresh.addActionListener(buttonAction);
+ markRead.addActionListener(buttonAction);
+ close.addActionListener(buttonAction);
+
+ pack();
+ setMinimumSize(new Dimension(400, 300));
+ setTitle("Announcements");
+
+ Timer timer = new Timer(TIMER_DELAY, new ActionListener() {
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ autoRequestNews(false);
+ }
+ });
+ timer.setRepeats(true);
+ timer.start();
+ }
+
+ /**
+ * Shows the dialog centered on the main GUI and requests the latest news if
+ * the request delay has passed.
+ */
+ public void showDialog() {
+ setLocationRelativeTo(main);
+ setVisible(true);
+ getNews(false);
+ }
+
+ public void autoRequestNews(boolean openIfUnread) {
+ if (settings.getBoolean("newsAutoRequest")) {
+ getNews(openIfUnread);
+ }
+ }
+
+ /**
+ * Requests the news from the server if the request delay has passed or
+ * load up a cached version.
+ *
+ * @param openIfUnread Open up the dialog automatically if new announcements
+ * are available
+ */
+ private void getNews(boolean openIfUnread) {
+ if (System.currentTimeMillis() - lastRequested < REQUEST_DELAY) {
+ setNews(cachedNews);
+ return;
+ }
+ lastRequested = System.currentTimeMillis();
+ requestNews(openIfUnread);
+ }
+
+ /**
+ * Requests the announcements from the server.
+ */
+ private void requestNews(boolean openIfUnread) {
+ news.setText("Loading..");
+ latestNewsTimestamp = 0;
+
+ UrlRequest request = new UrlRequest(NEWS_URL) {
+
+ @Override
+ public void requestResult(String result, int responseCode) {
+ SwingUtilities.invokeLater(new Runnable() {
+
+ @Override
+ public void run() {
+ if (responseCode == 200) {
+ int unread = setNews(result);
+ if (openIfUnread && unread > 0) {
+ showDialog();
+ }
+ } else {
+ news.setText("Error loading news. ("+responseCode+")");
+ }
+ }
+ });
+ }
+ };
+ new Thread(request).start();
+ }
+
+ /**
+ * Parses the given data and fills the dialog accordingly. This may be
+ * either directly from the server or a cached version. Updates the title
+ * and main menu notification accordingly.
+ *
+ * @param data The JSON containing the announcements
+ * @return The number of new announcements
+ */
+ private int setNews(String data) {
+ if (data == null || data.isEmpty()) {
+ return 0;
+ }
+ try {
+ int newCount = parseNews(data);
+ cachedNews = data;
+ if (newCount > 0) {
+ setTitle(String.format("Announcements (%d new)", newCount));
+ main.setAnnouncementAvailable(true);
+ } else {
+ setTitle("Announcements");
+ main.setAnnouncementAvailable(false);
+ }
+ } catch (Exception ex) {
+ news.setText("Error loading news.");
+ LOGGER.warning(MiscUtil.getStackTrace(ex));
+ }
+ return 0;
+ }
+
+ private int parseNews(String text) throws ParseException {
+
+ // Data
+ long lastRead = settings.getLong(SETTING_LAST_READ_TIMESTAMP);
+ int unreadCount = 0;
+
+ JSONParser parser = new JSONParser();
+ JSONObject data = (JSONObject)parser.parse(text);
+ JSONArray list = (JSONArray)data.get("news");
+
+ // HTML
+ final SimpleDateFormat DATETIME = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+
+ StringBuilder sb = new StringBuilder(""
+ + ""
+ + ""+parseContent((String)data.get("intro"))
+ + "
");
+
+ for (Object o : list) {
+
+ // Data
+ JSONObject entry = (JSONObject)o;
+ String title = (String)entry.get("title");
+ String content = (String)entry.get("content");
+ long time_added = ((Number)entry.get("timestamp")).longValue()*1000;
+ boolean old = JSONUtil.getBoolean(entry, "old", false);
+
+ if (time_added > lastRead && !old) {
+ unreadCount++;
+ }
+ if (time_added > latestNewsTimestamp) {
+ latestNewsTimestamp = time_added;
+ }
+
+ // HTML
+ sb.append("").append(title);
+ if (old) {
+ sb.append(" (old)");
+ }
+ else if (time_added > lastRead) {
+ sb.append(" (new)");
+ }
+
+ sb.append("
");
+ sb.append(" ");
+ sb.append(DATETIME.format(new Date(time_added)));
+ sb.append(" ("+DateTime.agoText(time_added)+")");
+ sb.append("
");
+ sb.append("").append(parseContent(content)).append("
");
+ }
+ sb.append("");
+
+ // Replace text in dialog
+ news.setDocument(news.getEditorKit().createDefaultDocument());
+ news.setText(sb.toString());
+ SwingUtilities.invokeLater(new Runnable() {
+
+ @Override
+ public void run() {
+ news.scrollRectToVisible(new Rectangle(0, 0, 1, 1));
+ }
+ });
+
+ return unreadCount;
+ }
+
+ private String parseContent(String content) {
+ return content.replaceAll("\\[([^]]+)\\]\\(([^)]+)\\)", "$1");
+ }
+
+}
diff --git a/src/chatty/gui/components/help/help-addressbook.html b/src/chatty/gui/components/help/help-addressbook.html
index 679295e64..56470f3d4 100644
--- a/src/chatty/gui/components/help/help-addressbook.html
+++ b/src/chatty/gui/components/help/help-addressbook.html
@@ -5,11 +5,23 @@
-
- Editing Locally (Commands) |
- Advanced Usage (Mod Commands,
- Change via file, Unique Categories)
-
+
The addressbook allows you to add usernames and assign categories to
them, which can then be used in other places such as the Usercolor settings
@@ -173,7 +185,7 @@
several categories by comma: /set abUniqueCats star,gold
-
+
This is an experimental feature that adds Subscribers automatically to
an Addressbook category, depending on how many months they subscribed.
This works on Subscriber Notifications in chat, so you have to be in the
diff --git a/src/chatty/util/DateTime.java b/src/chatty/util/DateTime.java
index 4519e9e4a..69460205a 100644
--- a/src/chatty/util/DateTime.java
+++ b/src/chatty/util/DateTime.java
@@ -7,6 +7,7 @@
import java.util.Date;
import java.util.List;
import java.util.Locale;
+import java.util.concurrent.TimeUnit;
/**
* Stuff to do with dates/time.
@@ -77,8 +78,12 @@ public static String agoText(long time) {
long hours = seconds / HOUR;
return hours+" "+(hours == 1 ? "hour" : "hours")+" ago";
}
- long days = seconds / DAY;
- return days+" "+(days == 1 ? "day" : "days")+" ago";
+ if (seconds < YEAR) {
+ long days = seconds / DAY;
+ return days + " " + (days == 1 ? "day" : "days") + " ago";
+ }
+ long years = seconds / YEAR;
+ return years+" "+(years == 1 ? "year" : "years")+" ago";
}
public static String agoClock(long time, boolean showSeconds) {
@@ -283,5 +288,6 @@ public static final void main(String[] args) {
// } catch (ParseException ex) {
// Logger.getLogger(DateTime.class.getName()).log(Level.SEVERE, null, ex);
// }
+ System.out.println(TimeUnit.HOURS.toMillis(1));
}
}
diff --git a/src/chatty/util/JSONUtil.java b/src/chatty/util/JSONUtil.java
index 057cd674d..66a018826 100644
--- a/src/chatty/util/JSONUtil.java
+++ b/src/chatty/util/JSONUtil.java
@@ -43,4 +43,12 @@ public static int getInteger(JSONObject data, Object key, int errorValue) {
return errorValue;
}
+ public static boolean getBoolean(JSONObject data, Object key, boolean errorValue) {
+ Object value = data.get(key);
+ if (value != null && value instanceof Boolean) {
+ return (Boolean)value;
+ }
+ return errorValue;
+ }
+
}
diff --git a/src/chatty/util/MiscUtil.java b/src/chatty/util/MiscUtil.java
index 9f8c935a1..d120f2cd8 100644
--- a/src/chatty/util/MiscUtil.java
+++ b/src/chatty/util/MiscUtil.java
@@ -8,6 +8,8 @@
import java.awt.datatransfer.StringSelection;
import java.io.File;
import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
@@ -103,4 +105,10 @@ public static void moveFile(Path from, Path to) {
}
}
}
+
+ public static String getStackTrace(Throwable e) {
+ StringWriter sw = new StringWriter();
+ e.printStackTrace(new PrintWriter(sw));
+ return sw.toString();
+ }
}