diff --git a/gemp-lotr/gemp-lotr-async/pom.xml b/gemp-lotr/gemp-lotr-async/pom.xml index 87466a287..f709d675a 100644 --- a/gemp-lotr/gemp-lotr-async/pom.xml +++ b/gemp-lotr/gemp-lotr-async/pom.xml @@ -32,6 +32,21 @@ gson 2.9.0 + + org.commonmark + commonmark + 0.19.0 + + + org.commonmark + commonmark-ext-gfm-strikethrough + 0.19.0 + + + org.commonmark + commonmark-ext-autolink + 0.19.0 + diff --git a/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/ChatRequestHandler.java b/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/ChatRequestHandler.java index e221278bf..2beb867d2 100644 --- a/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/ChatRequestHandler.java +++ b/gemp-lotr/gemp-lotr-async/src/main/java/com/gempukku/lotro/async/handler/ChatRequestHandler.java @@ -8,7 +8,6 @@ import com.gempukku.lotro.chat.ChatCommandErrorException; import com.gempukku.lotro.chat.ChatMessage; import com.gempukku.lotro.chat.ChatRoomMediator; import com.gempukku.lotro.chat.ChatServer; -import com.gempukku.lotro.game.CardSets; import com.gempukku.lotro.game.ChatCommunicationChannel; import com.gempukku.lotro.game.Player; import com.gempukku.polling.LongPollingResource; @@ -17,7 +16,18 @@ import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder; -import org.apache.commons.lang.StringEscapeUtils; +import org.commonmark.Extension; +import org.commonmark.ext.autolink.AutolinkExtension; +import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; +import org.commonmark.node.Image; +import org.commonmark.node.Link; +import org.commonmark.node.Node; +import org.commonmark.node.Text; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.NodeRenderer; +import org.commonmark.renderer.html.HtmlNodeRendererContext; +import org.commonmark.renderer.html.HtmlRenderer; +import org.commonmark.renderer.html.HtmlWriter; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -25,18 +35,32 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.lang.reflect.Type; import java.net.URLDecoder; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.*; +import java.util.regex.Pattern; public class ChatRequestHandler extends LotroServerRequestHandler implements UriRequestHandler { private final ChatServer _chatServer; private final LongPollingSystem longPollingSystem; + private final Parser _markdownParser; + private final HtmlRenderer _markdownRenderer; public ChatRequestHandler(Map context, LongPollingSystem longPollingSystem) { super(context); _chatServer = extractObject(context, ChatServer.class); this.longPollingSystem = longPollingSystem; + + List adminExt = Arrays.asList(StrikethroughExtension.create(), AutolinkExtension.create()); + _markdownParser = Parser.builder() + .extensions(adminExt) + .build(); + _markdownRenderer = HtmlRenderer.builder() + .nodeRendererFactory(htmlContext -> new LinkShredder(htmlContext)) + .extensions(adminExt) + .escapeHtml(true) + .sanitizeUrls(true) + .softbreak("
") + .build(); } @Override @@ -50,6 +74,8 @@ public class ChatRequestHandler extends LotroServerRequestHandler implements Uri } } + private Pattern QuoteExtender = Pattern.compile("^([ \t]*>[ \t]*.+)(?=\n[ \t]*[^>])", Pattern.MULTILINE); + private void postMessages(HttpRequest request, String room, ResponseWriter responseWriter) throws Exception { HttpPostRequestDecoder postDecoder = new HttpPostRequestDecoder(request); try { @@ -64,8 +90,17 @@ public class ChatRequestHandler extends LotroServerRequestHandler implements Uri try { final boolean admin = resourceOwner.getType().contains("a"); + final boolean leagueAdmin = resourceOwner.getType().contains("l"); if (message != null && message.trim().length() > 0) { - chatRoom.sendMessage(resourceOwner.getName(), StringEscapeUtils.escapeHtml(message), admin); + String newMsg; + newMsg = message.trim().replaceAll("\n\n\n+", "\n\n\n"); + newMsg = QuoteExtender.matcher(newMsg).replaceAll("$1\n"); + //Escaping underscores so that URLs with lots of underscores (i.e. wiki links) aren't mangled + // Besides, who uses _this_ instead of *this*? + newMsg = newMsg.replace("_", "\\_"); + newMsg = _markdownRenderer.render(_markdownParser.parse(newMsg)); + + chatRoom.sendMessage(resourceOwner.getName(), newMsg, admin); responseWriter.writeXmlResponse(null); } else { ChatCommunicationChannel pollableResource = chatRoom.getChatRoomListener(resourceOwner.getName()); @@ -84,6 +119,57 @@ public class ChatRequestHandler extends LotroServerRequestHandler implements Uri } } + //Processing to implement: + // + quotes restricted to one line + // - triple quote to avoid this?? + // + remove url text processing + // + remove image processing + // - re-enable bare url linking + + private class LinkShredder implements NodeRenderer { + + private final HtmlWriter html; + + LinkShredder(HtmlNodeRendererContext context) { + this.html = context.getWriter(); + } + + @Override + public Set> getNodeTypes() { + // Return the node types we want to use this renderer for. + return new HashSet<>(Arrays.asList( + Link.class, + Image.class + )); + } + + @Override + public void render(Node node) { + if(node instanceof Link link) { + if(link.getTitle() != null) { + html.text(link.getTitle() + ": " + link.getDestination()); + } + else { + if(link.getFirstChild() != null + && link.getFirstChild() instanceof Text text + && !text.getLiteral().equals(link.getDestination())) + { + html.text(text.getLiteral() + ": " + link.getDestination()); + } + else { + html.tag("a", Collections.singletonMap("href", link.getDestination())); + html.text(link.getDestination()); + html.tag("/a"); + } + } + + } + else if(node instanceof Image image){ + html.text(image.getTitle() + ": " + image.getDestination()); + } + } + } + private class ChatUpdateLongPollingResource implements LongPollingResource { private final ChatRoomMediator chatRoom; private final String room; diff --git a/gemp-lotr/gemp-lotr-async/src/main/web/css/gemp-001/hall.css b/gemp-lotr/gemp-lotr-async/src/main/web/css/gemp-001/hall.css index b3ef78e73..a86233b80 100644 --- a/gemp-lotr/gemp-lotr-async/src/main/web/css/gemp-001/hall.css +++ b/gemp-lotr/gemp-lotr-async/src/main/web/css/gemp-001/hall.css @@ -122,7 +122,6 @@ html, body { } .timestamp { - display: inline; color: #cfcfcf; } @@ -235,6 +234,8 @@ tr.played { #chatTalk { flex-grow: 1; + resize: none; + max-height: 60px; } #discordChat { @@ -257,11 +258,13 @@ tr.played { .flex-horiz { display: flex; flex-direction: row; + gap: 2px; } .flex-vert { display: flex; flex-direction: column; + gap: 2px; } #tabs{ position: sticky; @@ -279,26 +282,22 @@ tr.played { gap: 4px; } -#gameHall { - +.hall-tab { display: flex; flex-direction: column; flex-grow: 1; gap: 5px; -} - -#hall { - flex-grow: 1; + overflow: scroll; } #main { top: 5px; - height: auto; + height: 100%; min-height: 100%; + max-height: 100%; width: 100%; display: flex; flex-direction: column; - } #disclaimer { @@ -314,3 +313,62 @@ tr.played { #tablesDiv { overflow: auto; } + +a { + color: orange !important; +} + +.chatMessage h1, +.chatMessage h2, +.chatMessage h3, +.chatMessage h4, +.chatMessage h5, +.chatMessage h6 { + display: inline; + margin-top: 0; + margin-bottom: 0; +} + +.chatMessage p { + margin-top: 0; + margin-bottom: 0; +} + +.chatMessage blockquote { + display: inline-block; + border-left: 5px solid #505050; + margin-left: 5px; + margin-top: 0px; + margin-bottom: 5px; + font-style: italic; +} +.chatMessage blockquote br { + line-height: 0px; + margin: 0px; + content: ""; +} + +.user-mention { + background-color: #FFFF0044; +} + +.user-ping { + background-color: #FF000044; +} + +.message { + display: flex; + flex-direction: row; + gap: 2px; + align-items: flex-start; +} + +.msg-content p { + margin-left: 5px; + margin-top: 0px; + margin-bottom: 7px; +} + +.msg-identifier { + min-width: 200px; +} \ No newline at end of file diff --git a/gemp-lotr/gemp-lotr-async/src/main/web/hall.html b/gemp-lotr/gemp-lotr-async/src/main/web/hall.html index fbfd98143..acfe6a368 100644 --- a/gemp-lotr/gemp-lotr-async/src/main/web/hall.html +++ b/gemp-lotr/gemp-lotr-async/src/main/web/hall.html @@ -44,6 +44,7 @@ + @@ -60,7 +61,7 @@ chat = new ChatBoxUI("Game Hall", $("#chat"), "/gemp-lotr-server", true, null, false, true, null, true); chat.showTimestamps = true; - hall = new GempLotrHallUI($("#hall"), "/gemp-lotr-server", chat); + hall = new GempLotrHallUI("/gemp-lotr-server", chat); $("#main").tabs(); @@ -123,9 +124,9 @@ - -->
-
+
-
+
@@ -237,7 +238,8 @@
-
+
+
Deck builder | Merchant @@ -258,7 +260,7 @@
- + @@ -277,7 +279,6 @@
-
The information presented on this site about LotR TCG, both literal and graphical, is copyrighted by Decipher Inc. This website is not produced, endorsed, supported, or affiliated with Decipher. diff --git a/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/chat.js b/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/chat.js index 20790c645..57d830d11 100644 --- a/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/chat.js +++ b/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/chat.js @@ -1,6 +1,9 @@ var ChatBoxUI = Class.extend({ name:null, userName:null, + pingRegex:null, + mentionRegex:null, + everyoneRegex:/(@everyone|@anyone)/, div:null, comm:null, @@ -59,6 +62,13 @@ var ChatBoxUI = Class.extend({ that.appendMessage("Unknown chat problem occured (error=" + xhr.status + ")", "warningMessage"); }); this.enableDiscord = allowDiscord; + + this.comm.getPlayerInfo(function(json) + { + that.userName = json; + that.pingRegex = new RegExp("@" + json); + that.mentionRegex = new RegExp("(?)" + json ); + }, this.chatErrorMap()); @@ -66,13 +76,7 @@ var ChatBoxUI = Class.extend({ if(this.name == "Game Hall") { - - this.discordDiv = $("#discordChat"); - this.comm.getPlayerInfo(function(json) - { - that.userName = json; - }, this.chatErrorMap()); this.chatTalkDiv = $("#chatTalk"); @@ -128,13 +132,16 @@ var ChatBoxUI = Class.extend({ that.processMessages(xml, true); }, this.chatErrorMap()); - this.chatTalkDiv.bind("keypress", function (e) { - var code = (e.keyCode ? e.keyCode : e.which); - if (code == 13) { - var value = $(this).val(); - if (value != "") - that.sendMessage(value); - $(this).val(""); + this.chatTalkDiv.keydown(function (e) { + if (e.keyCode == 13) { + if(!e.shiftKey) + { + e.preventDefault(); + var value = $(this).val(); + if (value != "") + that.sendMessage(value); + $(this).val("").trigger("oninput"); + } } }); @@ -153,8 +160,6 @@ var ChatBoxUI = Class.extend({ } else { - - this.chatTalkDiv = $(""); if (showHideSystemButton) { @@ -242,10 +247,6 @@ var ChatBoxUI = Class.extend({ } }, - escapeHtml:function (text) { - return $('
').text(text).html(); - }, - setBounds:function (x, y, width, height) { if(this.name != "Game Hall") @@ -359,10 +360,21 @@ var ChatBoxUI = Class.extend({ } } }, + appendMessage:function (message, msgClass) { if (msgClass == undefined) msgClass = "chatMessage"; + + if(this.pingRegex != null && this.pingRegex.test(message)) + { + msgClass += " user-ping"; + } + else if((this.mentionRegex != null && this.mentionRegex.test(message)) || this.everyoneRegex.test(message)) + { + msgClass += " user-mention"; + } + var messageDiv = $("
" + message + "
"); this.chatMessagesDiv.append(messageDiv); @@ -402,13 +414,17 @@ var ChatBoxUI = Class.extend({ var msgClass = "chatMessage"; if (from == "System") msgClass = "systemMessage"; + var prefix = "
"; if (this.showTimestamps) { var date = new Date(parseInt(message.getAttribute("date"))); var dateStr = this.monthNames[date.getMonth()] + " " + date.getDate() + " " + this.formatToTwoDigits(date.getHours()) + ":" + this.formatToTwoDigits(date.getMinutes()) + ":" + this.formatToTwoDigits(date.getSeconds()); - this.appendMessage("
[" + dateStr + "]
" + from + ": " + text, msgClass); - } else { - this.appendMessage("" + from + ": " + text, msgClass); + prefix += "[" + dateStr + "]"; } + + prefix += " " + from + ":
"; + var postfix = "
" + text + "
"; + + this.appendMessage(prefix + postfix, msgClass); } var users = root.getElementsByTagName("user"); diff --git a/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/hallUi.js b/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/hallUi.js index ce553c3d9..bc81b5010 100644 --- a/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/hallUi.js +++ b/gemp-lotr/gemp-lotr-async/src/main/web/js/gemp-022/hallUi.js @@ -1,5 +1,4 @@ var GempLotrHallUI = Class.extend({ - div:null, comm:null, chat:null, supportedFormatsInitialized:false, @@ -15,8 +14,7 @@ var GempLotrHallUI = Class.extend({ pocketValue:null, hallChannelId: null, - init:function (div, url, chat) { - this.div = div; + init:function (url, chat) { this.comm = new GempLotrCommunication(url, function (xhr, ajaxOptions, thrownError) { if (thrownError != "abort") { if (xhr != null) { @@ -35,9 +33,6 @@ var GempLotrHallUI = Class.extend({ }); this.chat = chat; - var width = $(div).width(); - var height = $(div).height(); - this.tablesDiv = $("#tablesDiv"); var hallSettingsStr = $.cookie("hallSettings"); diff --git a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/chat/ChatRoomMediator.java b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/chat/ChatRoomMediator.java index 8d7a8ac6d..f2b9b04e2 100644 --- a/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/chat/ChatRoomMediator.java +++ b/gemp-lotr/gemp-lotr-server/src/main/java/com/gempukku/lotro/chat/ChatRoomMediator.java @@ -90,8 +90,8 @@ public class ChatRoomMediator { } public void sendMessage(String playerId, String message, boolean admin) throws PrivateInformationException, ChatCommandErrorException { - if (message.startsWith("/")) { - processIfKnownCommand(playerId, message.substring(1), admin); + if (message.trim().startsWith("/")) { + processIfKnownCommand(playerId, message.trim().substring(1), admin); return; }