Skip to content

Commit 7ff6b27

Browse files
committed
- New features for the TranscriptWindow and TranscriptWindowInterceptors:
- Enable TranscriptWindowInterceptors to apply different background/foreground colors - and more generally different styles - to different parts of a message text displayed in the TranscriptWindow, using the syntax of Asciidoc Custom Inline Styles ( https://docs.asciidoctor.org/asciidoc/latest/text/custom-inline-styles/ ), i.e. '[.style-name]#my styled text#' will apply the style 'style-name' to the text between ''#' characters. - Enable TranscriptWindowInterceptors to intercept messages that were added to a chat before the user joined the chat. - Fixed the issue in chatrooms that if the message is coming from the chat server (e.g. the server rejected the message for security reasons) and not a room occupant, an exception is thrown and the message is not displayed in chat window.
1 parent 6294835 commit 7ff6b27

5 files changed

Lines changed: 150 additions & 20 deletions

File tree

core/pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
<exclude>**/org/jivesoftware/sparkimpl/preference/media/*</exclude>
3636
<exclude>**/org/jivesoftware/sparkimpl/plugin/phone/JMFInit.*</exclude>
3737
</excludes>
38+
<source>11</source>
39+
<target>11</target>
3840
</configuration>
3941
</plugin>
4042
<plugin>

core/src/main/java/org/jivesoftware/spark/ui/MessageEntry.java

Lines changed: 78 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333
import java.awt.*;
3434
import java.awt.image.BufferedImage;
3535
import java.time.ZonedDateTime;
36-
import java.util.List;
3736
import java.util.*;
37+
import java.util.List;
3838
import java.util.concurrent.TimeUnit;
39+
import java.util.function.Predicate;
40+
import java.util.regex.Pattern;
3941

4042
import static javax.swing.text.StyleConstants.Foreground;
4143

@@ -51,12 +53,19 @@
5153
*/
5254
public class MessageEntry extends TimeStampedEntry
5355
{
54-
public static final List<Character> DIRECTIVE_CHARS = Arrays.asList( '*', '_', '~', '`' );
56+
public static final List<Character> DIRECTIVE_CHARS = Arrays.asList( '*', '_', '~', '`', '#', '[' );
57+
private static final Predicate<String> CUSTOM_STYLE_NAME_MATCHER = Pattern.compile("_?[a-zA-Z]((-|_)?[a-zA-Z0-9]+)*").asMatchPredicate();
58+
private static final SimpleAttributeSet DEFAULT_HIGHLIGHT_STYLE = new SimpleAttributeSet();
59+
static {
60+
StyleConstants.setBackground(DEFAULT_HIGHLIGHT_STYLE, Color.YELLOW);
61+
}
62+
5563
protected final String prefix;
5664
protected final Color prefixColor;
5765
protected final String message;
5866
protected final Color messageColor;
5967
protected final Color backgroundColor;
68+
protected final Map<String, AttributeSet> customStyles;
6069

6170
/**
6271
* Creates a new entry using the default background color (white/transparent).
@@ -102,9 +111,9 @@ public MessageEntry( ZonedDateTime timestamp, String prefix, Color prefixColor,
102111
this( timestamp, false, prefix, prefixColor, message, messageColor,backgroundColor );
103112

104113
}
105-
114+
106115
/**
107-
* Creates a new entry using the default background color (white/transparent).
116+
* Creates a new entry using a given background color.
108117
*
109118
* @param timestamp The timestamp of the entry (cannot be null).
110119
* @param isDelayed Set true if entry contain delayed delivery, historic timestamp.
@@ -115,13 +124,35 @@ public MessageEntry( ZonedDateTime timestamp, String prefix, Color prefixColor,
115124
* @param backgroundColor The color to be used for the entire entry (prefix as well ass message text).
116125
*/
117126
public MessageEntry( ZonedDateTime timestamp, boolean isDelayed, String prefix, Color prefixColor, String message, Color messageColor, Color backgroundColor )
127+
{
128+
this(timestamp, isDelayed, prefix, prefixColor, message, messageColor, backgroundColor, Map.of());
129+
}
130+
131+
/**
132+
* Creates a new entry using a given background color and optional custom styles to be applied like
133+
* * <a href="https://docs.asciidoctor.org/asciidoc/latest/text/custom-inline-styles/">Asciidoc Custom Inline Styles</a>:
134+
* * <p>{@code [.style-name]#my styled text#} will have the effect of applying the style corresponding to {@code style-name} in {@code customStyles} parameter, to the text between {@code #} characters.
135+
*
136+
* @param timestamp The timestamp of the entry (cannot be null).
137+
* @param isDelayed Set true if entry contain delayed delivery, historic timestamp.
138+
* @param prefix The prefix of the message (typically, the name of the author of the message.
139+
* @param prefixColor The color to be used for the timestamp and prefix text.
140+
* @param message The message text itself.
141+
* @param messageColor The color to be used for the message text.
142+
* @param customStyles custom styles to be applied to specific parts of the message in the same manner as Asciidoc custom inline styles.
143+
*/
144+
public MessageEntry( ZonedDateTime timestamp, boolean isDelayed, String prefix, Color prefixColor, String message, Color messageColor, Color backgroundColor, Map<String, AttributeSet> customStyles )
118145
{
119146
super( timestamp, isDelayed );
120147
this.prefix = prefix == null ? "" : prefix;
121148
this.prefixColor = prefixColor;
122149
this.message = message;
123150
this.messageColor = messageColor;
124151
this.backgroundColor = backgroundColor != null ? backgroundColor : new Color( 255, 255, 255, 0);
152+
if(customStyles.keySet().stream().anyMatch(styleName -> !CUSTOM_STYLE_NAME_MATCHER.test(styleName))) {
153+
throw new IllegalArgumentException("One of the input custom style names is invalid: " + customStyles.keySet());
154+
}
155+
this.customStyles = customStyles;
125156
}
126157

127158
protected MutableAttributeSet getPrefixStyle()
@@ -192,10 +223,45 @@ protected void addTo( ChatArea chatArea ) throws BadLocationException
192223
// Handle directives
193224
if (DIRECTIVE_CHARS.contains(line.charAt(from))) {
194225
char directive = line.charAt(from);
195-
to = line.indexOf(directive, from + 1);
196-
if (to != -1 && !Character.isWhitespace(line.charAt(to - 1)) && (to - from) > 1) {
197-
insertFragment(chatArea, line.substring(from + 1, to++), applyMessageStyle(directive, messageStyle));
198-
continue;
226+
/*
227+
If the matched directive char is [, check whether this is '[.style-name]#sometext#', where style-name is one of the names in customStyles, in which case apply that custom inline style (Asciidoc-like)
228+
*/
229+
final AttributeSet fragmentSpecificStyle;
230+
if(directive == '[') {
231+
if(line.charAt(from+1) == '.') {
232+
// style-name is at least one character, therefore look for ] from 'from+3' position
233+
final int closingSquareBracketIndex = line.indexOf(']', from + 3);
234+
if (closingSquareBracketIndex != -1 && line.charAt(closingSquareBracketIndex + 1) == '#') {
235+
final String appliedStyleName = line.substring(from + 2, closingSquareBracketIndex);
236+
if (!customStyles.containsKey(appliedStyleName)) {
237+
Log.error("The message text is using a custom inline style named '" + appliedStyleName + "' but no such style has been defined. Ignoring.");
238+
fragmentSpecificStyle = null;
239+
} else {
240+
fragmentSpecificStyle = customStyles.get(appliedStyleName);
241+
directive = '#';
242+
from = closingSquareBracketIndex + 1;
243+
Log.debug("Applying custom inline style '" + appliedStyleName + "' to the text between #");
244+
}
245+
} else {
246+
fragmentSpecificStyle = null;
247+
}
248+
} else {
249+
fragmentSpecificStyle = null;
250+
}
251+
} else if(directive == '#') {
252+
/*
253+
If the matched directive char is #, highlight (in yellow) the text between # (no custom inline style here, as it was already handled in previous case above).
254+
*/
255+
fragmentSpecificStyle = DEFAULT_HIGHLIGHT_STYLE;
256+
} else {
257+
fragmentSpecificStyle = applyMessageStyle(directive, messageStyle);
258+
}
259+
if(fragmentSpecificStyle != null) {
260+
to = line.indexOf(directive, from + 1);
261+
if (to != -1 && !Character.isWhitespace(line.charAt(to - 1)) && (to - from) > 1) {
262+
insertFragment(chatArea, line.substring(from + 1, to++), fragmentSpecificStyle);
263+
continue;
264+
}
199265
}
200266
}
201267

@@ -226,7 +292,7 @@ protected void addTo( ChatArea chatArea ) throws BadLocationException
226292
// chatArea.setCaretPosition( doc.getLength() );
227293
}
228294

229-
protected void insertFragment(ChatArea chatArea, String fragment, MutableAttributeSet style) throws BadLocationException {
295+
protected void insertFragment(ChatArea chatArea, String fragment, AttributeSet style) throws BadLocationException {
230296
if (insertPicture(chatArea, fragment, style)) return;
231297
if (insertLink(chatArea.getDocument(), fragment, style)) return;
232298
if (insertAddress(chatArea.getDocument(), fragment, style)) return;
@@ -367,7 +433,7 @@ public MutableAttributeSet applyMessageStyle( char directive, MutableAttributeSe
367433
* @param url - the link to the content to insert( ex. http://example.org/hello.gif )
368434
* @throws BadLocationException if the location is not available for insertion.
369435
*/
370-
public boolean insertPicture(ChatArea chatArea, String url, MutableAttributeSet messageStyle) throws BadLocationException
436+
public boolean insertPicture(ChatArea chatArea, String url, AttributeSet messageStyle) throws BadLocationException
371437
{
372438
// FIXME: this is unsafe. Do not blindly accept anything that looks like an URL (check if it is a valid URL).
373439
// TODO: instead of operating on message text content, operate on message stanza metadata.
@@ -469,7 +535,7 @@ public static ImageIcon scaleImage(ImageIcon icon, int w, int h)
469535
* @param link - the link to insert( ex. http://www.javasoft.com )
470536
* @throws BadLocationException if the location is not available for insertion.
471537
*/
472-
public boolean insertLink(Document doc, String link, MutableAttributeSet style) throws BadLocationException
538+
public boolean insertLink(Document doc, String link, AttributeSet style) throws BadLocationException
473539
{
474540
if ((link.startsWith("http://") ||
475541
link.startsWith("ftp://") ||
@@ -495,7 +561,7 @@ public boolean insertLink(Document doc, String link, MutableAttributeSet style)
495561
* @param address - the address to insert( ex. \superpc\etc\file\ OR http://localhost/ )
496562
* @throws BadLocationException if the location is not available for insertion.
497563
*/
498-
public Boolean insertAddress(Document doc, String address, MutableAttributeSet style) throws BadLocationException
564+
public Boolean insertAddress(Document doc, String address, AttributeSet style) throws BadLocationException
499565
{
500566
if (address.startsWith("\\\\") ||
501567
(address.indexOf("://") > 0 && address.indexOf(".") < 1)) {

core/src/main/java/org/jivesoftware/spark/ui/TranscriptWindow.java

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
import org.jivesoftware.spark.util.ModelUtil;
3030
import org.jivesoftware.spark.util.TaskEngine;
3131
import org.jivesoftware.spark.util.log.Log;
32-
import org.jivesoftware.sparkimpl.plugin.manager.Enterprise;
3332
import org.jivesoftware.sparkimpl.plugin.emoticons.EmoticonManager;
33+
import org.jivesoftware.sparkimpl.plugin.manager.Enterprise;
3434
import org.jivesoftware.sparkimpl.settings.local.LocalPreferences;
3535
import org.jivesoftware.sparkimpl.settings.local.SettingsManager;
3636
import org.jxmpp.util.XmppStringUtils;
@@ -336,12 +336,59 @@ public Date getLastUpdated()
336336
* @param message the message to insert.
337337
* @param date the timestamp of the message.
338338
*/
339-
public void insertHistoryMessage( String userid, String message, Date date )
339+
public void insertHistoryMessage( String userid, Message message, Date date )
340+
{
341+
for ( TranscriptWindowInterceptor interceptor : SparkManager.getChatManager().getTranscriptWindowInterceptors() )
342+
{
343+
try
344+
{
345+
boolean handled = interceptor.isHistoryMessageIntercepted( this, userid, message, date );
346+
if ( handled )
347+
{
348+
// Do nothing.
349+
return;
350+
}
351+
}
352+
catch ( Exception e )
353+
{
354+
Log.error( "A TranscriptWindowInterceptor ('" + interceptor + "') threw an exception while processing a chat history message (current user: '" + userid + "').", e );
355+
}
356+
}
357+
358+
final ZonedDateTime sentDate = date.toInstant().atZone( ZoneOffset.UTC );
359+
final Color historyColor = (Color) UIManager.get( "History.foreground" );
360+
361+
add( new MessageEntry( sentDate, true, userid, historyColor, message.getBody(), historyColor ) );
362+
}
363+
364+
/**
365+
* Adds a historic text message to this transcript window. These typically are messages that were added to a chat before the local user joined the chat.
366+
*
367+
* @param userid the userid of the sender.
368+
* @param message the message to insert.
369+
* @param date the timestamp of the message.
370+
*/
371+
public void insertHistoryMessage( String userid, String message, Date date)
372+
{
373+
insertHistoryMessage(userid, message, date, Map.of());
374+
}
375+
376+
/**
377+
* Adds a tagged historic text message to this transcript window. This is the same as {@link #insertHistoryMessage(String, Message, Date)}, except custom style(s) can be applied to specific part(s) of the message using Asciidoc-like
378+
* <a href="https://docs.asciidoctor.org/asciidoc/latest/text/custom-inline-styles/">Custom Inline Styles</a>:
379+
* <p>{@code [.style-name]#my styled text#} will have the effect of applying the style corresponding to {@code style-name} in {@code customStyles} parameter, to the text between {@code #} characters.
380+
*
381+
* @param userid the userid of the sender.
382+
* @param message the message to insert.
383+
* @param date the timestamp of the message.
384+
* @param customStyles custom styles to be applied to the parts of the message between {@code #} and following {@code [.style-name]} where {@code style-name} is the key identifying the style attributes in this parameter.
385+
*/
386+
public void insertHistoryMessage( String userid, String message, Date date, Map<String, AttributeSet> customStyles)
340387
{
341388
final ZonedDateTime sentDate = date.toInstant().atZone( ZoneOffset.UTC );
342389
final Color historyColor = (Color) UIManager.get( "History.foreground" );
343390

344-
add( new MessageEntry( sentDate, true, userid, historyColor, message, historyColor ) );
391+
add( new MessageEntry( sentDate, true, userid, historyColor, message, historyColor, null, customStyles ) );
345392
}
346393

347394
public void insertHorizontalLine()

core/src/main/java/org/jivesoftware/spark/ui/TranscriptWindowInterceptor.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import org.jivesoftware.smack.packet.Message;
1919

20+
import java.util.Date;
21+
2022
/**
2123
* Allows users to intercept messages before they are inserted into the TranscriptWindow.
2224
*
@@ -35,4 +37,18 @@ public interface TranscriptWindowInterceptor {
3537
*/
3638
boolean isMessageIntercepted(TranscriptWindow window, String userid, Message message);
3739

40+
/**
41+
* Is called before a historic text message by this user is inserted into the TranscriptWindow.
42+
* History messages are typically messages that were added to a chat before the local user joined the chat.
43+
*
44+
* @param window the TranscriptWindow.
45+
* @param userid the userid.
46+
* @param message the message to be inserted.
47+
* @param date the timestamp of the message
48+
* @return true if it should be handled by a custom interceptor.
49+
*/
50+
default boolean isHistoryMessageIntercepted(TranscriptWindow window, String userid, Message message, Date date)
51+
{
52+
return false;
53+
}
3854
}

core/src/main/java/org/jivesoftware/spark/ui/rooms/GroupChatRoom.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
import org.jivesoftware.spark.ui.conferences.DataFormDialog;
4747
import org.jivesoftware.spark.ui.conferences.GroupChatParticipantList;
4848
import org.jivesoftware.spark.util.ModelUtil;
49-
import org.jivesoftware.spark.util.SwingWorker;
5049
import org.jivesoftware.spark.util.UIComponentRegistry;
5150
import org.jivesoftware.spark.util.log.Log;
5251
import org.jivesoftware.sparkimpl.settings.local.LocalPreferences;
@@ -626,14 +625,14 @@ private void handleMessagePacket( Stanza stanza )
626625

627626
if ( ModelUtil.hasLength( message.getBody() ) )
628627
{
629-
final Resourcepart from = message.getFrom().getResourceOrThrow();
628+
final Resourcepart from = message.getFrom().getResourceOrEmpty();
630629

631630
if ( inf != null )
632631
{
633632
// This is part of the MUC history. No need to add it to the transcript again.
634633

635634
// Add to the UI component that shows the chat.
636-
getTranscriptWindow().insertHistoryMessage( from.toString(), message.getBody(), sentDate );
635+
getTranscriptWindow().insertHistoryMessage( from.toString(), message, sentDate );
637636
}
638637
else
639638
{
@@ -643,9 +642,9 @@ private void handleMessagePacket( Stanza stanza )
643642
return;
644643
}
645644

646-
final boolean isFromRoom = !message.getFrom().hasNoResource();
645+
final boolean isFromRoomOccupant = message.getFrom().hasResource();
647646

648-
if ( !isFromRoom && SparkManager.getUserManager().getOccupant( this, from ) == null )
647+
if ( isFromRoomOccupant && SparkManager.getUserManager().getOccupant( this, from ) == null )
649648
{
650649
return;
651650
}

0 commit comments

Comments
 (0)