diff --git a/assets/linux/io.github.chatty.desktop b/assets/linux/io.github.chatty.desktop
new file mode 100644
index 000000000..a7231e037
--- /dev/null
+++ b/assets/linux/io.github.chatty.desktop
@@ -0,0 +1,12 @@
+[Desktop Entry]
+Version=1.0
+Type=Application
+
+Name=Chatty
+Comment=twitch.tv chat client
+Categories=Network;InstantMessaging;Chat
+
+Icon=chatty
+Exec=chatty
+Terminal=false
+StartupWMClass=chatty-Chatty
diff --git a/assets/linux/io.github.chatty.metainfo.xml b/assets/linux/io.github.chatty.metainfo.xml
new file mode 100644
index 000000000..63e1a4ff5
--- /dev/null
+++ b/assets/linux/io.github.chatty.metainfo.xml
@@ -0,0 +1,82 @@
+
+
+ io.github.chatty
+
+ Chatty
+ Twitch.tv chat client
+
+ MIT
+ GPL-3.0-or-later and MIT
+
+
+
+ Chatty is a chat software specifically made for Twitch, in the spirit of a classic IRC Client.
+
+
+ Basic Features
+
+
+ Join several channels in tabs, split views or popped out into separate windows Channel Favorites & History Log chat to file, TAB-Completion, Input History Flexible message Highlighting and Ignoring Customizable chat colors, font, line spacings, alternating backgrounds Choose between several Look&Feel, including Dark Mode
+
+
+ Watching
+
+
+ Get notified when channels you follow go live Easily open streams in your browser, or run Livestreamer (or the more up-to-date Streamlink) out of Chatty
+
+
+ Streaming
+
+
+ Set your stream title, game & tags (with custom Presets) and run commercials Write current stream uptime to a file and create Stream Marker, via configurable hotkey or Mod Command, to assist in making Stream Highlights List your 100 most recent followers/subscribers Viewerhistory graph of your current streaming session
+
+
+ Moderating
+
+
+ Click on nick to open customizable User Dialog, showing recent messages and basic account info Optional pause-chat-on-hover to avoid misclicks AutoMod support to approve/deny filtered messages Create Custom Commands and customize Context Menus
+
+
+ Emotes & Badges
+
+
+ FrankerFaceZ Emotes (& Mod Icons), BetterTTV Emotes (no Personal Emotes though) Unified Bot Badge (using multiple sources) Emote Dialog with Favorites, Subemotes, Channel-specific Emotes, and more.. Emote TAB-Completion using Shift-TAB (configurable) Enter Emoji codes like :thinking:, aided by TAB-Completion Locally hide/ignore individual Emotes or Badges or add your own
+
+
+ Other Features
+
+
+ Use Chatty in several languages, including English, German, French, Russian, Japanese, and more.. (the help and parts of the GUI aren't translated, translations thanks to contributers) SpeedRunsLive Race Viewer Global Hotkey support (Windows, Linux, Mac), e.g. to trigger a commerical or Custom Command
+
+
+
+ https://chatty.github.io/
+ https://chatty.github.io/help/help-troubleshooting.html#report
+ https://chatty.github.io/#faq
+ https://chatty.github.io/help/help.html
+ https://chatty.github.io/#contribute
+ https://chatty.github.io/#feedback
+ https://github.com/chatty/chatty
+ https://github.com/chatty/chatty#contributions
+
+ # TODO: Auto generate me from changelog!
+
+
+ io.github.chatty.desktop
+
+
+ https://chatty.github.io/img/stuff.png
+ Screenshot showing the general interface of chatty
+
+
+ https://chatty.github.io/img/Chatty_Split_View.jpg
+ Screenshot showing the split view functionality with multiple chats open
+
+
+ https://chatty.github.io/img/userdialog.png
+ Screenshot showing the moderation functionality
+
+
+
+
+
diff --git a/build.gradle b/build.gradle
index e79a02735..aa26b305b 100644
--- a/build.gradle
+++ b/build.gradle
@@ -100,6 +100,7 @@ task allPlatformsZip(type: Zip, group: 'build') {
from tasks.shadowJar.archivePath
from ('assets') {
exclude 'lib'
+ exclude 'linux'
}
destinationDirectory = releasesDir
diff --git a/src/chatty/Chatty.java b/src/chatty/Chatty.java
index 447109fbf..7f31988cf 100644
--- a/src/chatty/Chatty.java
+++ b/src/chatty/Chatty.java
@@ -57,7 +57,7 @@ public class Chatty {
* by points. May contain a single "b" for beta versions, which are counted
* as older (so 0.8.7b4 is older than 0.8.7).
*/
- public static final String VERSION = "0.26.0.168";
+ public static final String VERSION = "0.27.0.168";
/**
* Enable Version Checker (if you compile and distribute this yourself, you
diff --git a/src/chatty/TwitchClient.java b/src/chatty/TwitchClient.java
index b25eff02b..d2d8436fd 100644
--- a/src/chatty/TwitchClient.java
+++ b/src/chatty/TwitchClient.java
@@ -1950,7 +1950,7 @@ else if (command.equals("bantest")) {
// g.printMessage("test10", testUser, "longer message abc hmm fwef wef wef wefwe fwe ewfwe fwef wwefwef"
// + "fjwfjfwjefjwefjwef wfejfkwlefjwoefjwf wfjwoeifjwefiowejfef wefjoiwefj", false, null, 0);
} else if (command.equals("requestfollowers")) {
- api.getFollowers(parameter);
+ api.getFollowers(parameter, false);
} else if (command.equals("simulate2")) {
c.simulate(parameter);
} else if (command.equals("simulate")) {
@@ -2733,8 +2733,10 @@ private void handleModAction(ModeratorActionData data) {
chatLog.modAction(data);
User modUser = c.getUser(channel, data.created_by);
- modUser.addModAction(data);
- g.updateUserinfo(modUser);
+ if (!data.moderation_action.equals("acknowledge_warning")) {
+ modUser.addModAction(data);
+ g.updateUserinfo(modUser);
+ }
String bannedUsername = ModLogInfo.getBannedUsername(data);
if (bannedUsername != null) {
@@ -2751,6 +2753,19 @@ private void handleModAction(ModeratorActionData data) {
unbannedUser.addUnban(type, data.created_by);
g.updateUserinfo(unbannedUser);
}
+ if (data.moderation_action.equals("warn") && data.args.size() > 1) {
+ String warnedUsername = ModLogInfo.getTargetUsername(data);
+ if (warnedUsername != null) {
+ User warnedUser = c.getUser(channel, warnedUsername);
+ String reason = data.args.get(1);
+ warnedUser.addWarning(reason, data.created_by);
+ g.updateUserinfo(warnedUser);
+ }
+ }
+ if (data.moderation_action.equals("acknowledge_warning")) {
+ modUser.addWarningAcknowledged();
+ g.updateUserinfo(modUser);
+ }
}
}
diff --git a/src/chatty/TwitchCommands.java b/src/chatty/TwitchCommands.java
index 96b0cdc4f..10b757029 100644
--- a/src/chatty/TwitchCommands.java
+++ b/src/chatty/TwitchCommands.java
@@ -165,6 +165,13 @@ public void addNewCommands(Commands commands, TwitchClient client) {
api.shoutout(user, resultListener);
}, "");
});
+ commands.add("warn", " ", p -> {
+ Commands.CommandParsedArgs args = p.parsedArgs(2, 2);
+ String reason = args != null ? args.get(1, "") : "";
+ userCommand(client, p, args, (user, resultListener) -> {
+ api.warn(user, reason, resultListener);
+ }, StringUtil.aEmptyb(reason, "", " (%s)"));
+ });
//--------------------------
// Broadcaster
//--------------------------
diff --git a/src/chatty/User.java b/src/chatty/User.java
index 65fcd44f3..a6d2fc5ca 100644
--- a/src/chatty/User.java
+++ b/src/chatty/User.java
@@ -390,6 +390,16 @@ public synchronized void addInfo(String message, String fullText) {
addLine(new InfoMessage(System.currentTimeMillis(), message, fullText));
}
+ public synchronized void addWarning(String reason, String by) {
+ setFirstSeen();
+ addLine(new WarnMessage(System.currentTimeMillis(), reason, by));
+ }
+
+ public synchronized void addWarningAcknowledged() {
+ setFirstSeen();
+ addLine(new WarnMessage(System.currentTimeMillis(), null, null));
+ }
+
public synchronized void addModAction(ModeratorActionData data) {
setFirstSeen();
addLine(new ModAction(System.currentTimeMillis(), data.moderation_action+" "+ModLogInfo.makeArgsText(data)));
@@ -1419,6 +1429,26 @@ public ModAction(long time, String commandAndParameters) {
}
+ public static class WarnMessage extends Message {
+
+ public final String reason;
+ public final String by;
+
+ /**
+ * If reason and by are null, the user has acknowledge a warning.
+ *
+ * @param time The timestamp the warning was received
+ * @param reason The message associated with the warning
+ * @param by The moderator who has issued the warning
+ */
+ public WarnMessage(long time, String reason, String by) {
+ super(time);
+ this.reason = reason;
+ this.by = by;
+ }
+
+ }
+
public static class AutoModMessage extends Message {
public final String message;
diff --git a/src/chatty/gui/MainGui.java b/src/chatty/gui/MainGui.java
index 5d2933536..0aa90113d 100644
--- a/src/chatty/gui/MainGui.java
+++ b/src/chatty/gui/MainGui.java
@@ -4352,7 +4352,7 @@ public void printLowTrustUserInfo(User user, final LowTrustUserMessageData data)
);
printMessage(user, data.text, false, tags);
}
-
+
//--------------------------
// Appended Info
//--------------------------
diff --git a/src/chatty/gui/MainMenu.java b/src/chatty/gui/MainMenu.java
index c2e85d458..8adeddefc 100644
--- a/src/chatty/gui/MainMenu.java
+++ b/src/chatty/gui/MainMenu.java
@@ -423,13 +423,18 @@ public void updateLayouts(Map layouts) {
public void updateCustomTabs(List infos) {
customTabsMenu.removeAll();
- for (RoutingTargetInfo info : infos) {
- String label = info.name;
- if (info.messages > -1) {
- label = String.format("%s (%d)",
- info.name, info.messages);
+ if (infos.isEmpty()) {
+ addItem(customTabsMenu, "", "No custom tabs").setEnabled(false);
+ }
+ else {
+ for (RoutingTargetInfo info : infos) {
+ String label = info.name;
+ if (info.messages > -1) {
+ label = String.format("%s (%d)",
+ info.name, info.messages);
+ }
+ addItem(customTabsMenu, "customTab." + info.name, label);
}
- addItem(customTabsMenu, "customTab."+info.name, label);
}
}
diff --git a/src/chatty/gui/components/FollowersDialog.java b/src/chatty/gui/components/FollowersDialog.java
index 5a71a558c..fdb0f46ff 100644
--- a/src/chatty/gui/components/FollowersDialog.java
+++ b/src/chatty/gui/components/FollowersDialog.java
@@ -10,6 +10,8 @@
import chatty.gui.GuiUtil;
import chatty.gui.laf.LaF;
import chatty.gui.MainGui;
+import chatty.gui.components.admin.AdminDialog;
+import static chatty.gui.components.admin.AdminDialog.SMALL_BUTTON_INSETS;
import chatty.gui.components.menus.ContextMenu;
import chatty.gui.components.menus.ContextMenuListener;
import chatty.gui.components.menus.StreamsContextMenu;
@@ -34,6 +36,7 @@
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.BufferedWriter;
@@ -49,6 +52,8 @@
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import javax.swing.BorderFactory;
+import javax.swing.ImageIcon;
+import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
@@ -96,6 +101,7 @@ public String toString() {
private final JTable table;
private final ListTableModel followers = new MyListTableModel();
private final JLabel loadInfo = new JLabel();
+ private final JButton refreshButton;
private final JScrollPane accessInfo;
private final JScrollPane mainTable;
@@ -192,16 +198,26 @@ public FollowersDialog(Type type, MainGui owner, final TwitchApi api,
accessInfo = new JScrollPane(accessInfoText);
mainPanel.add(accessInfo, gbc);
- gbc = GuiUtil.makeGbc(0, 3, 2, 1, GridBagConstraints.WEST);
+ gbc = GuiUtil.makeGbc(0, 3, 1, 1, GridBagConstraints.WEST);
gbc.insets = new Insets(2, 5, 5, 5);
mainPanel.add(loadInfo, gbc);
+ refreshButton = new JButton(Language.getString("admin.button.reload"));
+ refreshButton.setMargin(SMALL_BUTTON_INSETS);
+ refreshButton.setIcon(new ImageIcon(AdminDialog.class.getResource("view-refresh.png")));
+ refreshButton.setMnemonic(KeyEvent.VK_R);
+ refreshButton.addActionListener(e -> request(true));
+
+ gbc = GuiUtil.makeGbc(1, 3, 1, 1, GridBagConstraints.EAST);
+ gbc.insets = new Insets(2, 5, 5, 5);
+ mainPanel.add(refreshButton, gbc);
+
// Timer
Timer timer = new Timer(REFRESH_TIMER, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
- request();
+ request(false);
update();
table.repaint();
}
@@ -457,13 +473,13 @@ private void setColumnWidth(int column, int width, int minwidth, int maxwidth) {
/**
* Try to request new data if the dialog is open and a stream is set.
*/
- private void request() {
+ private void request(boolean forceRefresh) {
if (isVisible() && stream != null && !stream.isEmpty()) {
loading = true;
if (type == Type.FOLLOWERS) {
- api.getFollowers(stream);
+ api.getFollowers(stream, forceRefresh);
} else if (type == Type.SUBSCRIBERS) {
- api.getSubscribers(stream);
+ api.getSubscribers(stream, forceRefresh);
}
}
}
@@ -521,7 +537,7 @@ public void showDialog(String stream) {
updateMain();
}
setVisible(true);
- request();
+ request(false);
update();
}
diff --git a/src/chatty/gui/components/help/help-releases.html b/src/chatty/gui/components/help/help-releases.html
index 6ff395be7..289653a84 100644
--- a/src/chatty/gui/components/help/help-releases.html
+++ b/src/chatty/gui/components/help/help-releases.html
@@ -17,6 +17,7 @@
+ 0.27 |
0.26 |
0.25 |
0.24.1 |
@@ -76,7 +77,13 @@
full list of changes.
+ TBD
+
+
Custom Tabs
diff --git a/src/chatty/gui/components/help/help.html b/src/chatty/gui/components/help/help.html
index 1ab29dc95..ee93d0c00 100644
--- a/src/chatty/gui/components/help/help.html
+++ b/src/chatty/gui/components/help/help.html
@@ -5,7 +5,7 @@
-
+
diff --git a/src/chatty/gui/components/textpane/ModLogInfo.java b/src/chatty/gui/components/textpane/ModLogInfo.java
index d85fe5e22..d797997b9 100644
--- a/src/chatty/gui/components/textpane/ModLogInfo.java
+++ b/src/chatty/gui/components/textpane/ModLogInfo.java
@@ -183,6 +183,16 @@ public static String getUnbannedUsername(ModeratorActionData data) {
return null;
}
+ public static String getTargetUsername(ModeratorActionData data) {
+ if (!data.args.isEmpty()) {
+ String username = data.args.get(0);
+ if (Helper.isValidStream(username)) {
+ return username;
+ }
+ }
+ return null;
+ }
+
@Override
public String toString() {
return text;
diff --git a/src/chatty/gui/components/userinfo/PastMessages.java b/src/chatty/gui/components/userinfo/PastMessages.java
index 45289e73c..90907c12d 100644
--- a/src/chatty/gui/components/userinfo/PastMessages.java
+++ b/src/chatty/gui/components/userinfo/PastMessages.java
@@ -254,6 +254,17 @@ else if (m instanceof User.InfoMessage) {
b.append(sm.full_text);
b.append("\n");
}
+ else if (m instanceof User.WarnMessage) {
+ User.WarnMessage wm = (User.WarnMessage)m;
+ b.append(timestampFormat.make(m.getTime(), user.getRoom())).append("! ");
+ if (wm.by == null && wm.reason == null) {
+ b.append("Warnung acknowledged");
+ }
+ else {
+ b.append("Warned by ").append(wm.by).append(" (").append(wm.reason).append(")");
+ }
+ b.append("\n");
+ }
else if (m instanceof User.ModAction) {
User.ModAction ma = (User.ModAction)m;
b.append(timestampFormat.make(m.getTime(), user.getRoom())).append(">");
diff --git a/src/chatty/gui/components/userinfo/UserInfoManager.java b/src/chatty/gui/components/userinfo/UserInfoManager.java
index 599044f6e..e337e36b4 100644
--- a/src/chatty/gui/components/userinfo/UserInfoManager.java
+++ b/src/chatty/gui/components/userinfo/UserInfoManager.java
@@ -80,7 +80,7 @@ public UserInfo getCachedUserInfo(String channel, Consumer result) {
@Override
public void requestFollowerInfo(String stream) {
- api.getFollowers(stream);
+ api.getFollowers(stream, false);
}
};
diff --git a/src/chatty/lang/Strings.properties b/src/chatty/lang/Strings.properties
index f5cfa45db..ed0f98b51 100644
--- a/src/chatty/lang/Strings.properties
+++ b/src/chatty/lang/Strings.properties
@@ -374,6 +374,7 @@ login.access.manageColor = Change color
login.access.manageRaids = Start/cancel Raids
login.access.managePolls = Manage/view Polls
login.access.manageShoutouts = Perform/view Shoutouts
+login.access.manageWarnings = Warn users
login.access.viewFollowers = View followers
login.access.editClips = Create clips
login.accessCategory.basic = Basic
diff --git a/src/chatty/util/api/FollowerManager.java b/src/chatty/util/api/FollowerManager.java
index 875deb785..2b078885c 100644
--- a/src/chatty/util/api/FollowerManager.java
+++ b/src/chatty/util/api/FollowerManager.java
@@ -124,14 +124,15 @@ private void noError(String stream) {
* be updated, in which case it requests the data from the API.
*
* @param streamName The name of the stream to request the data for
+ * @param forceRefresh
*/
- protected synchronized void request(String streamName) {
+ protected synchronized void request(String streamName, boolean forceRefresh) {
if (streamName == null || streamName.isEmpty()) {
return;
}
final String stream = StringUtil.toLowerCase(streamName);
FollowerInfo cachedInfo = cached.get(stream);
- if (cachedInfo == null || checkTimePassed(cachedInfo)) {
+ if (cachedInfo == null || checkTimePassed(cachedInfo) || forceRefresh) {
api.userIDs.getUserIDsAsap(r -> {
if (!r.hasError()) {
String streamId = r.getId(stream);
diff --git a/src/chatty/util/api/Requests.java b/src/chatty/util/api/Requests.java
index d5560c164..d17f143fc 100644
--- a/src/chatty/util/api/Requests.java
+++ b/src/chatty/util/api/Requests.java
@@ -585,6 +585,20 @@ public void shoutout(String streamId, String targetId, SimpleRequestResultListen
});
}
+ public void warn(String streamId, String targetId, String reason, SimpleRequestResultListener listener) {
+ String url = makeUrl("https://api.twitch.tv/helix/moderation/warnings",
+ "broadcaster_id", streamId,
+ "moderator_id", api.localUserId);
+ JSONObject data = new JSONObject();
+ data.put("user_id", targetId);
+ data.put("reason", reason);
+ JSONObject json = new JSONObject();
+ json.put("data", data);
+ newApi.add(url, "POST", json.toJSONString(), api.defaultToken, r -> {
+ handleResult(r, listener);
+ });
+ }
+
public void setVip(String streamId, String targetId, boolean add, SimpleRequestResultListener listener) {
String url = makeUrl("https://api.twitch.tv/helix/channels/vips",
"broadcaster_id", streamId,
diff --git a/src/chatty/util/api/ResultManager.java b/src/chatty/util/api/ResultManager.java
index 187bd6d3c..ff35d1e8f 100644
--- a/src/chatty/util/api/ResultManager.java
+++ b/src/chatty/util/api/ResultManager.java
@@ -40,12 +40,11 @@ public void subscribe(Type type, Object listener) {
}
/**
- * Subscribe to the given type, but remove any previous listener under the
- * same type with the same unique object provided.
+ * Subscribe to the given type, but remove any previous listener with the
+ * same unique object provided.
*
* @param type
- * @param unique If provided, remove previous listener with the same type
- * and unique object
+ * @param unique If provided remove previous listener added with this object
* @param listener
*/
public void subscribe(Type type, Object unique, Object listener) {
@@ -59,7 +58,7 @@ public void subscribe(Type type, Object unique, Object listener) {
if (unique != null) {
Object prevListener = uniqueListeners.remove(unique);
if (prevListener != null) {
- listeners.get(type).remove(prevListener);
+ listeners.forEach((k,v) -> v.remove(prevListener));
}
uniqueListeners.put(unique, listener);
}
diff --git a/src/chatty/util/api/TokenInfo.java b/src/chatty/util/api/TokenInfo.java
index ae2a95ec5..76191e5e0 100644
--- a/src/chatty/util/api/TokenInfo.java
+++ b/src/chatty/util/api/TokenInfo.java
@@ -31,6 +31,7 @@ public enum ScopeCategory {
),
MODERATION("moderation",
Scope.MANAGE_CHAT,
+ Scope.MANAGE_WARNINGS,
Scope.MANAGE_BANS,
Scope.MANAGE_MSGS,
Scope.CHAN_MOD,
@@ -106,6 +107,7 @@ public enum Scope {
MANAGE_RAIDS("channel:manage:raids", "manageRaids"),
MANAGE_POLLS("channel:manage:polls", "managePolls"),
MANAGE_SHOUTOUTS("moderator:manage:shoutouts", "manageShoutouts"),
+ MANAGE_WARNINGS("moderator:manage:warnings", "manageWarnings"),
CLIPS_EDIT("clips:edit", "editClips");
public String scope;
diff --git a/src/chatty/util/api/TwitchApi.java b/src/chatty/util/api/TwitchApi.java
index 4d768915a..984a87e1d 100644
--- a/src/chatty/util/api/TwitchApi.java
+++ b/src/chatty/util/api/TwitchApi.java
@@ -243,16 +243,16 @@ public void getStreamLabels() {
StreamLabels.request(requests);
}
- public void getFollowers(String stream) {
- followerManager.request(stream);
+ public void getFollowers(String stream, boolean forceRefresh) {
+ followerManager.request(stream, forceRefresh);
}
public Follower getSingleFollower(String stream, String streamId, String user, String userId, boolean refresh) {
return followerManager.getSingleFollower(stream, streamId, user, userId, refresh);
}
- public void getSubscribers(String stream) {
- subscriberManager.request(stream);
+ public void getSubscribers(String stream, boolean forceRefresh) {
+ subscriberManager.request(stream, forceRefresh);
}
public UserInfo getCachedUserInfo(String channel, Consumer result) {
@@ -592,6 +592,12 @@ public void shoutout(User targetUser, SimpleRequestResultListener listener) {
});
}
+ public void warn(User targetUser, String reason, SimpleRequestResultListener listener) {
+ runWithUserIds(targetUser, listener, (streamId, targetId) -> {
+ requests.warn(streamId, targetId, reason, listener);
+ });
+ }
+
public void setVip(User targetUser, boolean add, SimpleRequestResultListener listener) {
runWithUserIds(targetUser, listener, (streamId, targetId) -> {
requests.setVip(streamId, targetId, add, listener);
diff --git a/src/chatty/util/api/pubsub/ModeratorActionData.java b/src/chatty/util/api/pubsub/ModeratorActionData.java
index acd6c2905..4682199e6 100644
--- a/src/chatty/util/api/pubsub/ModeratorActionData.java
+++ b/src/chatty/util/api/pubsub/ModeratorActionData.java
@@ -139,6 +139,15 @@ public static ModeratorActionData decode(String topic, String message, Map