diff --git a/src/chatty/Commands.java b/src/chatty/Commands.java index cb7946ec6..4d170786d 100644 --- a/src/chatty/Commands.java +++ b/src/chatty/Commands.java @@ -188,6 +188,14 @@ public String getArgs() { return parameters.getArgs(); } + public String getArgsTrimNonNull() { + String args = StringUtil.trim(getArgs()); + if (args != null) { + return args; + } + return ""; + } + public boolean hasArgs() { return !StringUtil.isNullOrEmpty(parameters.getArgs()); } diff --git a/src/chatty/TwitchClient.java b/src/chatty/TwitchClient.java index c2b18aa5c..b25eff02b 100644 --- a/src/chatty/TwitchClient.java +++ b/src/chatty/TwitchClient.java @@ -73,6 +73,7 @@ import chatty.util.api.Follower; import chatty.util.api.FollowerInfo; import chatty.util.api.ResultManager; +import chatty.util.api.ResultManager.CreateClipResult; import chatty.util.api.StreamCategory; import chatty.util.api.StreamInfo.StreamType; import chatty.util.api.StreamInfo.ViewerStats; @@ -1629,9 +1630,18 @@ private void addCommands() { commandAddStreamMarker(p.getRoom(), p.getArgs()); }); commands.add("createClip", p -> { - api.createClip(p.getRoom().getStream(), result -> { - g.printLine(p.getRoom(), result); + api.subscribe(ResultManager.Type.CREATE_CLIP, commands, (CreateClipResult) (editUrl, viewUrl, error) -> { + if (error != null) { + g.printLine(p.getRoom(), error); + } + else { + // Update indices when changing info text + MsgTags tags = MsgTags.createLinks( + new MsgTags.Link(MsgTags.Link.Type.URL, editUrl, 14, 22)); + g.printInfo(p.getRoom(), "Clip created (Edit Clip): "+viewUrl, tags); + } }); + api.createClip(p.getRoom().getStream()); }); c.addNewCommands(commands, this); commands.add("addStreamHighlight", p -> { @@ -2109,7 +2119,7 @@ public void run() { } else if (command.equals("testr")) { api.test(); } else if (command.equals("joinlink")) { - MsgTags tags = MsgTags.create("chatty-channel-join", Helper.toChannel("twitch")); + MsgTags tags = MsgTags.createLinks(new MsgTags.Link(MsgTags.Link.Type.JOIN, Helper.toChannel("twitch"), "Join")); g.printInfo(c.getRoomByChannel(channel), "Join link:", tags); } } @@ -2753,7 +2763,7 @@ public void messageReceived(chatty.util.api.eventsub.Message message) { String channel = Helper.toChannel(raid.fromLogin); String text = String.format("[Raid] Now raiding %s with %d viewers.", raid.toLogin, raid.viewers); - MsgTags tags = MsgTags.create("chatty-channel-join", Helper.toChannel(raid.toLogin)); + MsgTags tags = MsgTags.createLinks(new MsgTags.Link(MsgTags.Link.Type.JOIN, Helper.toChannel(raid.toLogin), "Join")); g.printInfo(c.getRoomByChannel(channel), text, tags); } String pollMessage = PollPayload.getPollMessage(message); @@ -2784,7 +2794,7 @@ public void messageReceived(chatty.util.api.eventsub.Message message) { String infoText = String.format("[Shoutout] Was given to %s (@%s)", shoutout.target_name, shoutout.moderator_login); - MsgTags tags = MsgTags.create("chatty-channel-join", Helper.toChannel(shoutout.target_login)); + MsgTags tags = MsgTags.createLinks(new MsgTags.Link(MsgTags.Link.Type.JOIN, Helper.toChannel(shoutout.target_login), "Join")); g.printInfo(c.getRoomByChannel(channel), infoText, tags); // Mod Action List args = new ArrayList<>(); diff --git a/src/chatty/TwitchCommands.java b/src/chatty/TwitchCommands.java index 70f40bc1c..96b0cdc4f 100644 --- a/src/chatty/TwitchCommands.java +++ b/src/chatty/TwitchCommands.java @@ -6,6 +6,7 @@ import chatty.gui.UrlOpener; import chatty.lang.Language; import chatty.util.DateTime; +import chatty.util.RecentlyAffectedUsers; import chatty.util.StringUtil; import chatty.util.api.TwitchApi; import chatty.util.api.TwitchApi.SimpleRequestResultListener; @@ -58,7 +59,7 @@ public class TwitchCommands { private static final Set OTHER_COMMANDS = new HashSet<>(Arrays.asList(new String[]{ })); - private TwitchConnection c; + private final TwitchConnection c; public TwitchCommands(TwitchConnection c) { this.c = c; @@ -392,6 +393,7 @@ private void userCommand(TwitchClient client, if (r.error == null) { // Success client.g.addToLine(p.getRoom(), objectId, "OK"); + RecentlyAffectedUsers.addUser(user); } else { // Failed diff --git a/src/chatty/gui/MainGui.java b/src/chatty/gui/MainGui.java index 6a3617765..5d2933536 100644 --- a/src/chatty/gui/MainGui.java +++ b/src/chatty/gui/MainGui.java @@ -2731,10 +2731,14 @@ public void usericonClicked(Usericon usericon, MouseEvent e) { } @Override - public void linkClicked(Channel channel, String link) { - if (link.startsWith("join.")) { - String c = link.substring("join.".length()); - client.joinChannel(c); + public void linkClicked(Channel channel, MsgTags.Link link) { + switch (link.type) { + case JOIN: + client.joinChannel(link.target); + break; + case URL: + UrlOpener.openUrl(link.target); + break; } } @@ -2865,6 +2869,15 @@ else if (!Helper.isValidStream(username)) { openUserInfoDialog(user, p.getParameters().get("msg-id"), null); } }); + client.commands.addEdt("userinfoRecent", p -> { + User user = RecentlyAffectedUsers.poll(p.getChannel()); + if (user == null) { + printSystem(p.getRoom(), "No recently affected user found"); + } + else { + openUserInfoDialog(user, p.getParameters().get("msg-id"), null); + } + }); client.commands.addEdt("search", p -> { openSearchDialog(); }); diff --git a/src/chatty/gui/UserListener.java b/src/chatty/gui/UserListener.java index 7b081f6cc..2d61670fe 100644 --- a/src/chatty/gui/UserListener.java +++ b/src/chatty/gui/UserListener.java @@ -5,6 +5,7 @@ import chatty.gui.components.Channel; import chatty.util.api.Emoticon; import chatty.util.api.usericons.Usericon; +import chatty.util.irc.MsgTags; import java.awt.event.MouseEvent; /** @@ -21,7 +22,7 @@ public interface UserListener { public void userClicked(User user, String messageId, String autoModMsgId, MouseEvent e); public void emoteClicked(Emoticon emote, MouseEvent e); public void usericonClicked(Usericon usericon, MouseEvent e); - public void linkClicked(Channel channel, String link); + public void linkClicked(Channel channel, MsgTags.Link link); public void imageClicked(String url); } diff --git a/src/chatty/gui/components/help/help-builtin_commands.html b/src/chatty/gui/components/help/help-builtin_commands.html index 7f545f2bc..6b98c7a5c 100644 --- a/src/chatty/gui/components/help/help-builtin_commands.html +++ b/src/chatty/gui/components/help/help-builtin_commands.html @@ -124,6 +124,11 @@

Open windows in Chatty

/openSubscribers - Opens the according dialog
  • /userinfo <username> - To open the User Info Dialog on a specific user of the current channel (if available)
  • +
  • /userinfoRecent - To open the User Info Dialog on a + user recently affected by a Twitch Command or with recently open + User Dialog on that channel. Repeately executing the command (e.g. + through a hotkey) goes back in the history of recently affected + users, up to 10 users.
  • /releaseinfo - Opens the help with the release information
  • diff --git a/src/chatty/gui/components/help/help-releases.html b/src/chatty/gui/components/help/help-releases.html index 8e7608b02..6ff395be7 100644 --- a/src/chatty/gui/components/help/help-releases.html +++ b/src/chatty/gui/components/help/help-releases.html @@ -76,12 +76,46 @@

    Release Information

    full list of changes.

    - Version 0.26 (This one!) (2023-??-??) + Version 0.26 (This one!) (2024-03-21) [back to top]

    -
    -TBD
    -    
    +

    Custom Tabs

    +

    Custom Tabs allow you to route message from regular channels to a Custom Tab based on Highlight matching rules.

    +

    The new to: prefix (for Highlights, Ignore, Msg. Colours and the new Routing setting) specifies the name of the Custom Tab you want to route the matching messages to. For example adding to:Mentions regw:yourname to the Custom Tabs Routing list will copy all messages containing the word "yourname" to a tab called "Mentions".

    +

    There are also options to copy the text of Desktop Notifications to a Custom Tab, logging Custom Tab messages to a file and more.

    +

    Message History

    +

    Added a message history on channel join, which can be enabled in the Settings under "History". It can show regular chat messages from the last 24h for many channels based on the recent-messages.robotty.de API.

    +

    Channel Moderation Panel

    +

    Added an "M" button to the input field that opens a panel for changing channel modes (available only for moderators).

    +

    Twitch Features

    + +

    Other

    + +

    Bugfixes

    +

    Version 0.25 (2023-07-21) diff --git a/src/chatty/gui/components/help/help.html b/src/chatty/gui/components/help/help.html index 322c153a9..1ab29dc95 100644 --- a/src/chatty/gui/components/help/help.html +++ b/src/chatty/gui/components/help/help.html @@ -5,7 +5,7 @@ -

    Chatty (Version: 0.26-b6)

    +

    Chatty (Version: 0.26)

    diff --git a/src/chatty/gui/components/help/style.css b/src/chatty/gui/components/help/style.css index d1a1901b3..ace2ce9a5 100644 --- a/src/chatty/gui/components/help/style.css +++ b/src/chatty/gui/components/help/style.css @@ -3,7 +3,7 @@ body { font-size: 1em; color: black; background-color: #FDFDFD; - font-family: Arial, sans-serif; + font-family: sans-serif; padding: 10px; margin: 0; } diff --git a/src/chatty/gui/components/textpane/ChannelTextPane.java b/src/chatty/gui/components/textpane/ChannelTextPane.java index 9dc7b3153..c811fc4e7 100644 --- a/src/chatty/gui/components/textpane/ChannelTextPane.java +++ b/src/chatty/gui/components/textpane/ChannelTextPane.java @@ -77,6 +77,7 @@ import chatty.util.api.usericons.UsericonManager; import java.util.function.Function; import chatty.gui.transparency.TransparencyComponent; +import chatty.util.irc.MsgTags.Link; import java.util.function.Consumer; @@ -778,10 +779,10 @@ private void printInfoMessage2(InfoMessage message, AttributeSet style) { printTimestamp(style); printChannelIcon(null, message.localUser); printSpecialsInfo(message.text, style, message.highlightMatches, message.tags); - Pair link = message.getLink(); - if (link != null) { + + for (Link link : message.getAppendedLinks()) { print(" ", style); - print(link.key, styles.generalLink(style, link.value)); + print(link.label, styles.generalLink(style, link)); } finishLine(); } @@ -1948,7 +1949,7 @@ public void usericonClicked(Usericon usericon, MouseEvent e) { } @Override - public void linkClicked(Channel channel, String link) { + public void linkClicked(Channel channel, MsgTags.Link link) { } @Override @@ -2733,21 +2734,18 @@ private void findSpecialLinks(TreeMap ranges, HashMap links = tags.getLinks(); + if (links == null) { return; } - /** - * This only allows one link per message, just extending the existing - * join link functionality a bit, but should be good enough for now. - */ - String chan = tags.getChannelJoin(); - String indices = tags.getChannelJoinIndices(); - String[] split = indices.split("-"); - int start = Integer.parseInt(split[0]); - int end = Integer.parseInt(split[1]); - if (!inRanges(start, ranges) && !inRanges(end, ranges)) { - ranges.put(start, end); - rangesStyle.put(start, styles.generalLink(baseStyle, "join."+chan)); + + for (Link link : links) { + if (link.startIndex != -1 && link.endIndex != -1) { + if (!inRanges(link.startIndex, ranges) && !inRanges(link.endIndex, ranges)) { + ranges.put(link.startIndex, link.endIndex); + rangesStyle.put(link.startIndex, styles.generalLink(baseStyle, link)); + } + } } } @@ -4081,7 +4079,7 @@ public MutableAttributeSet info(Color color) { return info(); } - public MutableAttributeSet generalLink(AttributeSet base, String target) { + public MutableAttributeSet generalLink(AttributeSet base, MsgTags.Link target) { SimpleAttributeSet result = new SimpleAttributeSet(base); result.addAttribute(Attribute.GENERAL_LINK, target); StyleConstants.setUnderline(result, true); diff --git a/src/chatty/gui/components/textpane/InfoMessage.java b/src/chatty/gui/components/textpane/InfoMessage.java index 4652e8658..35b38e85f 100644 --- a/src/chatty/gui/components/textpane/InfoMessage.java +++ b/src/chatty/gui/components/textpane/InfoMessage.java @@ -5,7 +5,9 @@ import chatty.gui.Highlighter.Match; import chatty.util.Pair; import chatty.util.irc.MsgTags; +import chatty.util.irc.MsgTags.Link; import java.awt.Color; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -139,14 +141,16 @@ public int getMsgEnd() { return -1; } - public Pair getLink() { + public List getAppendedLinks() { + List result = new ArrayList<>(); if (tags != null) { - // When indicdes are set the join link is added differently - if (tags.getChannelJoin() != null && tags.getChannelJoinIndices() == null) { - return new Pair<>("Join", "join."+tags.getChannelJoin()); + for (Link link : tags.getLinks()) { + if (link.startIndex == -1) { + result.add(link); + } } } - return null; + return result; } } diff --git a/src/chatty/gui/components/textpane/LinkController.java b/src/chatty/gui/components/textpane/LinkController.java index e8a168010..30c0aebc3 100644 --- a/src/chatty/gui/components/textpane/LinkController.java +++ b/src/chatty/gui/components/textpane/LinkController.java @@ -37,6 +37,7 @@ import chatty.util.api.CachedImage; import chatty.util.api.CachedImage.ImageType; import chatty.util.api.usericons.Usericon; +import chatty.util.irc.MsgTags; import java.awt.Color; import java.awt.Component; import java.awt.Cursor; @@ -234,7 +235,7 @@ else if (e.isPopupTrigger()) { private void handleSingleLeftClick(MouseEvent e, Element element) { String url; - String link; + MsgTags.Link link; User user; CachedImage emoteImage; CachedImage usericonImage; @@ -395,8 +396,8 @@ private boolean isUrlDeleted(Element e) { return deleted; } - private String getGeneralLink(Element e) { - return (String)(e.getAttributes().getAttribute(ChannelTextPane.Attribute.GENERAL_LINK)); + private MsgTags.Link getGeneralLink(Element e) { + return (MsgTags.Link)(e.getAttributes().getAttribute(ChannelTextPane.Attribute.GENERAL_LINK)); } private User getUser(Element e) { @@ -552,7 +553,7 @@ private void openContextMenu(MouseEvent e) { user = getMention(element); } String url = getUrl(element); - String link = getGeneralLink(element); + MsgTags.Link link = getGeneralLink(element); CachedImage emoteImage = getEmoticonImage(element); CachedImage usericonImage = getUsericonImage(element); if (user != null) { @@ -563,9 +564,14 @@ else if (url != null) { m = new UrlContextMenu(url, isUrlDeleted(element), contextMenuListener); } else if (link != null) { - if (link.startsWith("join.")) { - String c = Helper.toStream(link.substring("join.".length())); - m = new StreamsContextMenu(Arrays.asList(new String[]{c}), contextMenuListener); + switch (link.type) { + case JOIN: + String c = Helper.toStream(link.target); + m = new StreamsContextMenu(Arrays.asList(new String[]{c}), contextMenuListener); + break; + case URL: + m = new UrlContextMenu(link.target, false, contextMenuListener); + break; } } else if (emoteImage != null) { diff --git a/src/chatty/gui/components/textpane/UserMessage.java b/src/chatty/gui/components/textpane/UserMessage.java index 5629b7c5f..e57b6ff6d 100644 --- a/src/chatty/gui/components/textpane/UserMessage.java +++ b/src/chatty/gui/components/textpane/UserMessage.java @@ -56,6 +56,7 @@ public UserMessage copy() { result.ignoreSource = ignoreSource; result.routingSource = routingSource; result.localUser = localUser; + result.historicTimeStamp = historicTimeStamp; return result; } diff --git a/src/chatty/gui/components/userinfo/UserInfoDialog.java b/src/chatty/gui/components/userinfo/UserInfoDialog.java index 55f5fed60..29d654bcd 100644 --- a/src/chatty/gui/components/userinfo/UserInfoDialog.java +++ b/src/chatty/gui/components/userinfo/UserInfoDialog.java @@ -54,8 +54,6 @@ public enum Action { private final JCheckBox singleMessage = new JCheckBox(SINGLE_MESSAGE_CHECK); private final BanReasons banReasons; private final Buttons buttons; - - private final ActionListener actionListener; private User currentUser; private String currentLocalUsername; @@ -83,6 +81,7 @@ public UserInfoDialog(final Window parent, UserInfoListener listener, Settings settings, final ContextMenuListener contextMenuListener) { super(parent); + setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); this.requester = requester; this.settings = settings; GuiUtil.installEscapeCloseOperation(this); @@ -107,14 +106,7 @@ public void actionPerformed(ActionEvent e) { } }); - actionListener = new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - dispose(); - } - }; - closeButton.addActionListener(actionListener); + closeButton.addActionListener(e -> dispose()); setLayout(new GridBagLayout()); diff --git a/src/chatty/gui/components/userinfo/UserInfoManager.java b/src/chatty/gui/components/userinfo/UserInfoManager.java index 965c5d916..599044f6e 100644 --- a/src/chatty/gui/components/userinfo/UserInfoManager.java +++ b/src/chatty/gui/components/userinfo/UserInfoManager.java @@ -6,8 +6,8 @@ import chatty.gui.GuiUtil; import chatty.gui.MainGui; import chatty.gui.components.menus.ContextMenuListener; +import chatty.util.RecentlyAffectedUsers; import chatty.util.Timestamp; -import chatty.util.api.ChannelInfo; import chatty.util.api.Follower; import chatty.util.api.FollowerInfo; import chatty.util.api.TwitchApi; @@ -18,10 +18,9 @@ import java.awt.Component; import java.awt.Point; import java.awt.Window; -import java.awt.event.ComponentAdapter; -import java.awt.event.ComponentEvent; -import java.awt.event.ComponentListener; -import java.text.SimpleDateFormat; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -33,13 +32,10 @@ public class UserInfoManager { private final List dialogs = new ArrayList<>(); - private final ComponentListener closeListener; + private final WindowListener closeListener; private final Window dummyWindow = new Window(null); - private int x; - private int y; - private final Point temp2 = new Point(); - + private final MainGui main; private final Settings settings; private final ContextMenuListener contextMenuListener; @@ -55,22 +51,13 @@ public UserInfoManager(final MainGui owner, Settings settings, this.main = owner; this.settings = settings; this.contextMenuListener = contextMenuListener; - closeListener = new ComponentAdapter() { + closeListener = new WindowAdapter() { @Override - public void componentHidden(ComponentEvent e) { - handleClosed(e.getComponent()); + public void windowClosed(WindowEvent e) { + handleClosed(e.getWindow()); } - @Override - public void componentMoved(ComponentEvent e) { -// handleChanged(e.getComponent()); - } - - @Override - public void componentResized(ComponentEvent e) { -// handleChanged(e.getComponent()); - } }; userInfoListener = new UserInfoListener() { @@ -215,7 +202,7 @@ private void setInitialLocationAndSize(UserInfoDialog dialog) { dialog.setSize(400, 360); } dialog.setLocation(targetLocation); - dialog.addComponentListener(closeListener); + dialog.addWindowListener(closeListener); } private void handleClosed(Component c) { @@ -229,6 +216,7 @@ private void handleClosed(Component c) { if (!dialog.isPinned() && numUnpinned() == 1) { saveLocationAndSize(dialog); } + RecentlyAffectedUsers.addUser(dialog.getUser()); } private void saveLocationAndSize(Component c) { diff --git a/src/chatty/gui/notifications/NotificationManager.java b/src/chatty/gui/notifications/NotificationManager.java index be69fc0cf..7a6fecdb2 100644 --- a/src/chatty/gui/notifications/NotificationManager.java +++ b/src/chatty/gui/notifications/NotificationManager.java @@ -417,9 +417,10 @@ private void addInfoMsg(Notification n, NotificationData d, String channel, Stri int start = StringUtil.toLowerCase(d.title).indexOf(stream); if (start != -1) { int end = start + stream.length() - 1; - tags = MsgTags.create( - "chatty-channel-join", stream, - "chatty-channel-join-indices", start+"-"+end); + tags = MsgTags.createLinks(new MsgTags.Link( + MsgTags.Link.Type.JOIN, + stream, + start, end)); } } diff --git a/src/chatty/util/RecentlyAffectedUsers.java b/src/chatty/util/RecentlyAffectedUsers.java new file mode 100644 index 000000000..c5c4b3312 --- /dev/null +++ b/src/chatty/util/RecentlyAffectedUsers.java @@ -0,0 +1,44 @@ + +package chatty.util; + +import chatty.User; +import java.util.HashMap; +import java.util.Map; + +/** + * Store users most recently affected by a command or their User Dialog being + * open, per channel. + * + * @author tduva + */ +public class RecentlyAffectedUsers { + + private static final Map> users = new HashMap<>(); + + /** + * Add a user, removing the user if already present, effectively moving it + * to the end (most recent) of the list. + * + * @param user + */ + public synchronized static void addUser(User user) { + if (!users.containsKey(user.getChannel())) { + users.put(user.getChannel(), new UniqueLimitedRingBuffer<>(10)); + } + users.get(user.getChannel()).append(user); + } + + /** + * Get and remove the most recent user for this channel. + * + * @param channel + * @return The {@code User} or {@code null} if none is present + */ + public synchronized static User poll(String channel) { + if (users.containsKey(channel)) { + return users.get(channel).pollLast(); + } + return null; + } + +} diff --git a/src/chatty/util/UniqueLimitedRingBuffer.java b/src/chatty/util/UniqueLimitedRingBuffer.java new file mode 100644 index 000000000..8658df89d --- /dev/null +++ b/src/chatty/util/UniqueLimitedRingBuffer.java @@ -0,0 +1,46 @@ + +package chatty.util; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Set; + +/** + * + * @author tduva + */ +public class UniqueLimitedRingBuffer { + + private final LinkedList list = new LinkedList<>(); + private final Set set = new HashSet<>(); + private final int capacity; + + public UniqueLimitedRingBuffer(int capacity) { + this.capacity = capacity; + } + + public void append(T e) { + if (set.add(e)) { + list.addLast(e); + } + else { + list.remove(e); + list.addLast(e); + } + if (list.size() > capacity) { + list.removeFirst(); + } + } + + public T pollLast() { + T removed = list.pollLast(); + set.remove(removed); + return removed; + } + + public void remove(T o) { + list.remove(o); + set.remove(o); + } + +} diff --git a/src/chatty/util/api/Requests.java b/src/chatty/util/api/Requests.java index 24fc4a6c7..d5560c164 100644 --- a/src/chatty/util/api/Requests.java +++ b/src/chatty/util/api/Requests.java @@ -32,6 +32,7 @@ import java.util.logging.Logger; import org.json.simple.JSONObject; import chatty.util.api.ResultManager.CategoryResult; +import chatty.util.api.ResultManager.CreateClipResult; import chatty.util.api.ResultManager.ShieldModeResult; import chatty.util.api.TokenInfo.Scope; import chatty.util.api.TwitchApi.SimpleRequestResult; @@ -468,31 +469,43 @@ public void createStreamMarker(String userId, String description, String token, }); } - public void createClip(String userId, Consumer listener) { + public void createClip(String userId) { String url = makeUrl("https://api.twitch.tv/helix/clips", "broadcaster_id", userId); newApi.add(url, "POST", api.defaultToken, r -> { + String error = null; if (r.responseCode == 202) { - String clipUrl = Parsing.getClipUrl(r.text); - if (clipUrl != null) { - listener.accept("Edit clip: "+clipUrl); + String editUrl = Parsing.getClipUrl(r.text); + if (editUrl != null) { + String viewUrl = editUrl.replace("/edit", ""); + api.resultManager.inform(ResultManager.Type.CREATE_CLIP, + (CreateClipResult l) -> { + l.result(editUrl, viewUrl, null); + }); } else { - listener.accept("Error creating clip"); + error = "Error creating clip"; } } else if (r.responseCode == 401) { - listener.accept("Creating clip failed: Check access under 'Main - Account'"); + error = "Creating clip failed: Check access under 'Main - Account'"; } else { String errorMsg = getErrorMessage(r.text); if (errorMsg != null) { - listener.accept(errorMsg); + error = errorMsg; } else { - listener.accept("Creating clip failed"); + error = "Creating clip failed"; } } + if (error != null) { + String error2 = error; + api.resultManager.inform(ResultManager.Type.CREATE_CLIP, + (CreateClipResult l) -> { + l.result(null, null, error2); + }); + } }); } diff --git a/src/chatty/util/api/ResultManager.java b/src/chatty/util/api/ResultManager.java index 8cc11b190..187bd6d3c 100644 --- a/src/chatty/util/api/ResultManager.java +++ b/src/chatty/util/api/ResultManager.java @@ -21,10 +21,12 @@ public class ResultManager { private final Map> listeners = new HashMap<>(); + private final Map uniqueListeners = new HashMap<>(); public enum Type { CATEGORY_RESULT(CategoryResult.class), - SHIELD_MODE_RESULT(ShieldModeResult.class); + SHIELD_MODE_RESULT(ShieldModeResult.class), + CREATE_CLIP(CreateClipResult.class); private final Class c; @@ -34,6 +36,19 @@ public enum Type { } public void subscribe(Type type, Object listener) { + subscribe(type, null, listener); + } + + /** + * Subscribe to the given type, but remove any previous listener under the + * same type with the same unique object provided. + * + * @param type + * @param unique If provided, remove previous listener with the same type + * and unique object + * @param listener + */ + public void subscribe(Type type, Object unique, Object listener) { if (listener != null) { if (!type.c.isInstance(listener)) { throw new RuntimeException("Invalid parameter"); @@ -41,6 +56,13 @@ public void subscribe(Type type, Object listener) { if (!listeners.containsKey(type)) { listeners.put(type, new HashSet<>()); } + if (unique != null) { + Object prevListener = uniqueListeners.remove(unique); + if (prevListener != null) { + listeners.get(type).remove(prevListener); + } + uniqueListeners.put(unique, listener); + } listeners.get(type).add(listener); } } @@ -63,6 +85,10 @@ public interface ShieldModeResult { public void result(String stream, boolean enabled); } + public interface CreateClipResult { + public void result(String editUrl, String viewUrl, String error); + } + public static void main(String[] args) { ResultManager m = new ResultManager(); m.subscribe(Type.CATEGORY_RESULT, (CategoryResult) categories -> { diff --git a/src/chatty/util/api/TwitchApi.java b/src/chatty/util/api/TwitchApi.java index f9ae198fa..4d768915a 100644 --- a/src/chatty/util/api/TwitchApi.java +++ b/src/chatty/util/api/TwitchApi.java @@ -11,6 +11,7 @@ import java.util.*; import java.util.logging.Logger; import chatty.util.api.ResultManager.CategoryResult; +import chatty.util.api.ResultManager.CreateClipResult; import chatty.util.api.eventsub.EventSubAddResult; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -531,12 +532,12 @@ public void createStreamMarker(String stream, String description, StreamMarkerRe }, stream); } - public void createClip(String stream, Consumer listener) { + public void createClip(String stream) { userIDs.getUserIDsAsap(r -> { if (r.hasError()) { - listener.accept("Failed to resolve channel id"); + resultManager.inform(ResultManager.Type.CREATE_CLIP, (CreateClipResult l) -> l.result(null, null, "Failed to resolve channel id")); } else { - requests.createClip(r.getId(stream), listener); + requests.createClip(r.getId(stream)); } }, stream); } @@ -559,6 +560,10 @@ public void subscribe(ResultManager.Type type, Object listener) { resultManager.subscribe(type, listener); } + public void subscribe(ResultManager.Type type, Object unique, Object listener) { + resultManager.subscribe(type, unique, listener); + } + public interface StreamMarkerResult { public void streamMarkerResult(String error); } diff --git a/src/chatty/util/irc/MsgTags.java b/src/chatty/util/irc/MsgTags.java index ff9c3d733..9f6ef495a 100644 --- a/src/chatty/util/irc/MsgTags.java +++ b/src/chatty/util/irc/MsgTags.java @@ -3,8 +3,11 @@ import chatty.util.StringUtil; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Objects; /** * Implementation of IRCv3 tags with Twitch-specific methods. @@ -13,10 +16,13 @@ */ public class MsgTags extends IrcMsgTags { - public static final MsgTags EMPTY = new MsgTags(null); + public static final MsgTags EMPTY = new MsgTags(null, null); + + private Map objects; - public MsgTags(Map tags) { + public MsgTags(Map tags, Map objects) { super(tags); + this.objects = objects; } public String getId() { @@ -63,14 +69,6 @@ public long getHistoricTimeStamp() { } } - public String getChannelJoin() { - return get("chatty-channel-join"); - } - - public String getChannelJoinIndices() { - return get("chatty-channel-join-indices"); - } - public boolean isRestrictedMessage() { return isValue("chatty-is-restricted", "1"); } @@ -119,27 +117,40 @@ public String getHypeChatInfo() { //================ /** - * Parse the given IRCv3 tags String (no leading @) into a IrcMsgTags object. + * Parse the given IRCv3 tags String (no leading @) into a MsgTags object. * * @param tags The tags String - * @return IrcMsgTags object, empty if tags was null + * @return MsgTags object, empty if tags was null */ public static MsgTags parse(String tags) { Map parsedTags = parseTags(tags); if (parsedTags == null) { return EMPTY; } - return new MsgTags(parsedTags); + return new MsgTags(parsedTags, null); } /** - * Create a new IrcMsgTags object with the given key/value pairs. + * Create a new MsgTags object with the given key/value pairs. * * @param args Alternating key/value pairs - * @return IrcMsgTags object + * @return MsgTags object */ public static MsgTags create(String... args) { - return new MsgTags(createTags(args)); + return new MsgTags(createTags(args), null); + } + + public void fillObjects(Map map) { + if (objects != null) { + map.putAll(objects); + } + } + + private void addObject(String key, Object value) { + if (objects == null) { + objects = new HashMap<>(); + } + objects.put(key, value); } /** @@ -154,7 +165,12 @@ public static MsgTags merge(MsgTags a, MsgTags b) { Map result = new HashMap<>(); b.fill(result); a.fill(result); - return new MsgTags(result); + + Map objectsResult = new HashMap<>(); + b.fillObjects(objectsResult); + a.fillObjects(objectsResult); + + return new MsgTags(result, objectsResult); } /** @@ -170,7 +186,107 @@ public static MsgTags addTag(MsgTags a, String key, String value) { Map result = new HashMap<>(); a.fill(result); result.put(key, value); - return new MsgTags(result); + + Map objectsResult = new HashMap<>(); + a.fillObjects(objectsResult); + + return new MsgTags(result, objectsResult); + } + + //======= + // Links + //======= + + @SuppressWarnings("unchecked") + public List getLinks() { + if (objects != null && objects.containsKey("links")) { + return (List) objects.get("links"); + } + return new ArrayList<>(); + } + + public static MsgTags createLinks(Link... input) { + MsgTags tags = create(""); + tags.addObject("links", createLinksObject(input)); + return tags; + } + + public static Object createLinksObject(Link... links) { + List result = new ArrayList<>(); + for (Link link : links) { + result.add(link); + } + return result; + } + + public static class Link { + + public enum Type { + JOIN, URL + } + + public final Type type; + public final String target; + public final String label; + public final int startIndex; + public final int endIndex; + + public Link(Type type, String target, int startIndex, int endIndex) { + this.type = type; + this.target = target; + this.label = ""; + this.startIndex = startIndex; + this.endIndex = endIndex; + } + + public Link(Type type, String target, String label) { + this.type = type; + this.target = target; + this.label = label; + this.startIndex = -1; + this.endIndex = -1; + } + + @Override + public String toString() { + return String.format("[%s.%s %s](%d-%d)", + type, target, label, startIndex, endIndex); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Link other = (Link) obj; + if (this.startIndex != other.startIndex) { + return false; + } + if (this.endIndex != other.endIndex) { + return false; + } + if (!Objects.equals(this.target, other.target)) { + return false; + } + return Objects.equals(this.label, other.label); + } + + @Override + public int hashCode() { + int hash = 7; + hash = 79 * hash + Objects.hashCode(this.target); + hash = 79 * hash + Objects.hashCode(this.label); + hash = 79 * hash + this.startIndex; + hash = 79 * hash + this.endIndex; + return hash; + } + } }