diff --git a/src/main/java/com/faforever/client/chat/ChatChannel.java b/src/main/java/com/faforever/client/chat/ChatChannel.java index fef37b9fd2..c177dedc25 100644 --- a/src/main/java/com/faforever/client/chat/ChatChannel.java +++ b/src/main/java/com/faforever/client/chat/ChatChannel.java @@ -145,6 +145,10 @@ public Optional getUser(String username) { return Optional.ofNullable(usernameToChatUser.get(username)); } + public Optional getMessage(String id) { + return Optional.ofNullable(messagesById.get(id)); + } + public void removePendingMessage(String messageId) { messagesById.computeIfPresent(messageId, (ignored, chatMessage) -> chatMessage.getType() == Type.PENDING ? null : chatMessage); diff --git a/src/main/java/com/faforever/client/chat/ChatMessage.java b/src/main/java/com/faforever/client/chat/ChatMessage.java index cdb11433c0..e2a4605a78 100644 --- a/src/main/java/com/faforever/client/chat/ChatMessage.java +++ b/src/main/java/com/faforever/client/chat/ChatMessage.java @@ -1,5 +1,9 @@ package com.faforever.client.chat; +import com.faforever.client.chat.emoticons.Emoticon; +import javafx.collections.FXCollections; +import javafx.collections.ObservableMap; +import javafx.collections.ObservableSet; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.RequiredArgsConstructor; @@ -8,15 +12,36 @@ @RequiredArgsConstructor @EqualsAndHashCode(onlyExplicitlyIncluded = true) -@Getter public class ChatMessage { @EqualsAndHashCode.Include + @Getter private final String id; + @Getter private final Instant time; + @Getter private final ChatChannelUser sender; + @Getter private final String content; + @Getter private final Type type; + @Getter + private final ChatMessage targetMessage; + + private final ObservableMap> reactions = FXCollections.synchronizedObservableMap( + FXCollections.observableHashMap()); + private final ObservableMap> unmodifiableReactions = FXCollections.unmodifiableObservableMap( + reactions); + + public ObservableMap> getReactions() { + return unmodifiableReactions; + } + + public void addReaction(Emoticon reaction, ChatChannelUser reactor) { + reactions.computeIfAbsent(reaction, + ignored -> FXCollections.synchronizedObservableSet(FXCollections.observableSet())) + .add(reactor.getUsername()); + } public enum Type { MESSAGE, ACTION, PENDING diff --git a/src/main/java/com/faforever/client/chat/ChatMessageController.java b/src/main/java/com/faforever/client/chat/ChatMessageController.java index a483060c34..bf24316cf2 100644 --- a/src/main/java/com/faforever/client/chat/ChatMessageController.java +++ b/src/main/java/com/faforever/client/chat/ChatMessageController.java @@ -2,16 +2,22 @@ import com.faforever.client.avatar.AvatarService; import com.faforever.client.chat.ChatMessage.Type; +import com.faforever.client.chat.emoticons.Emoticon; import com.faforever.client.chat.emoticons.EmoticonService; +import com.faforever.client.chat.emoticons.EmoticonsWindowController; import com.faforever.client.domain.AvatarBean; import com.faforever.client.domain.PlayerBean; +import com.faforever.client.fx.FxApplicationThreadExecutor; import com.faforever.client.fx.ImageViewHelper; import com.faforever.client.fx.JavaFxUtil; import com.faforever.client.fx.NodeController; import com.faforever.client.fx.PlatformService; import com.faforever.client.i18n.I18n; import com.faforever.client.player.CountryFlagService; +import com.faforever.client.theme.UiService; +import com.faforever.client.util.PopupUtil; import com.faforever.client.util.TimeService; +import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -19,17 +25,25 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.MapChangeListener; +import javafx.collections.ObservableSet; +import javafx.geometry.Bounds; import javafx.geometry.Insets; import javafx.scene.Node; +import javafx.scene.control.Button; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; +import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; +import javafx.stage.Popup; +import javafx.stage.PopupWindow.AnchorLocation; import javafx.util.Duration; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -39,7 +53,9 @@ import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.regex.Pattern; @Slf4j @@ -54,8 +70,10 @@ public class ChatMessageController extends NodeController { private final PlatformService platformService; private final ChatService chatService; private final EmoticonService emoticonService; + private final UiService uiService; private final ImageViewHelper imageViewHelper; private final I18n i18n; + private final FxApplicationThreadExecutor fxApplicationThreadExecutor; public VBox root; public HBox detailsContainer; @@ -65,11 +83,18 @@ public class ChatMessageController extends NodeController { public Label authorLabel; public Label timeLabel; public TextFlow message; + public HBox messageActionsContainer; + public Button reactButton; + public Button replyButton; + public FlowPane reactionsContainer; private final Tooltip avatarTooltip = new Tooltip(); private final ObjectProperty chatMessage = new SimpleObjectProperty<>(); private final BooleanProperty showDetails = new SimpleBooleanProperty(); private final StringProperty inlineTextColorStyleProperty = new SimpleStringProperty(); + private final MapChangeListener> reactionChangeListener = this::onReactionChange; + + private final Map reactionNodeMap = new HashMap<>(); private Pattern mentionPattern; @@ -104,7 +129,7 @@ protected void onInitialize() { authorLabel.setOnMouseClicked(event -> { String username = usernameProperty.getValue(); if (username != null && event.getClickCount() == 2) { - chatService.onInitiatePrivateChat(username); + chatService.joinPrivateChat(username); } }); timeLabel.textProperty() @@ -125,21 +150,66 @@ protected void onInitialize() { avatarTooltip.setShowDelay(Duration.ZERO); avatarTooltip.setShowDuration(Duration.seconds(30)); Tooltip.install(avatarImageView, avatarTooltip); + + chatMessage.subscribe((oldValue, newValue) -> { + if (oldValue != null) { + oldValue.getReactions().removeListener(reactionChangeListener); + } + + reactionNodeMap.clear(); + fxApplicationThreadExecutor.execute(() -> reactionsContainer.getChildren().clear()); + + if (newValue != null) { + newValue.getReactions().forEach(this::addReaction); + newValue.getReactions().addListener(reactionChangeListener); + } + }); + + reactionsContainer.visibleProperty() + .bind(chatMessage.map(ChatMessage::getReactions).flatMap(Bindings::isNotEmpty).when(showing)); + } + + private void onReactionChange(MapChangeListener.Change> change) { + Emoticon reaction = change.getKey(); + if (change.wasRemoved()) { + HBox reactionRoot = reactionNodeMap.remove(reaction); + fxApplicationThreadExecutor.execute(() -> reactionsContainer.getChildren().remove(reactionRoot)); + } + + if (change.wasAdded()) { + addReaction(reaction, change.getValueAdded()); + } + } + + private void addReaction(Emoticon reaction, ObservableSet reactors) { + ReactionController reactionController = uiService.loadFxml("theme/chat/emoticons/reaction.fxml"); + reactionController.setReaction(reaction); + reactionController.setReactors(reactors); + reactionController.onReactionClickedProperty() + .bind( + chatMessage.map(message -> react -> chatService.reactToMessageInBackground(message, react))); + HBox reactionRoot = reactionController.getRoot(); + reactionNodeMap.put(reaction, reactionRoot); + fxApplicationThreadExecutor.execute(() -> reactionsContainer.getChildren().add(reactionRoot)); + } + + public void onMouseExited() { + messageActionsContainer.setVisible(false); + } + + public void onMouseEntered() { + messageActionsContainer.setVisible(true); } private List convertMessageToNodes(String message) { - return Arrays.stream(message.split("\\s+")) - .map(this::convertWordToNode) - .peek(this::styleMessageNode) - .toList(); + return Arrays.stream(message.split("\\s+")).map(this::convertWordToNode).peek(this::styleMessageNode).toList(); } private Node convertWordToNode(String word) { return switch (word) { case String url when PlatformService.URL_REGEX_PATTERN.matcher(url).matches() -> createExternalHyperlink(url); case String channel when channel.startsWith("#") -> createChannelLink(channel); - case String shortcode when emoticonService.getEmoticonShortcodeDetectorPattern().matcher(shortcode).matches() -> - createEmoticon(shortcode); + case String shortcode when emoticonService.isEmoticonShortcode(shortcode) -> createEmoticon(shortcode); default -> new Text(word + " "); }; } @@ -203,4 +273,30 @@ public BooleanProperty showDetailsProperty() { public void setShowDetails(boolean showDetails) { this.showDetails.set(showDetails); } + + public void onReply() {} + + public void onReact() { + EmoticonsWindowController emoticonsWindowController = uiService.loadFxml( + "theme/chat/emoticons/emoticons_window.fxml"); + Popup emoticonsPopup = PopupUtil.createPopup(AnchorLocation.WINDOW_BOTTOM_RIGHT, + emoticonsWindowController.getRoot()); + emoticonsPopup.setConsumeAutoHidingEvents(false); + emoticonsWindowController.setOnEmoticonClicked(emoticon -> { + ChatMessage target = getChatMessage(); + if (target != null && !target.getReactions() + .getOrDefault(emoticon, FXCollections.emptyObservableSet()) + .contains(chatService.getCurrentUsername())) { + chatService.reactToMessageInBackground(target, emoticon); + } + emoticonsPopup.hide(); + }); + + Bounds bounds = reactButton.localToScreen(reactButton.getBoundsInLocal()); + if (bounds == null) { + return; + } + + emoticonsPopup.show(reactButton.getScene().getWindow(), bounds.getMaxX() - 5, bounds.getMinY() - 5); + } } diff --git a/src/main/java/com/faforever/client/chat/ChatMessageViewController.java b/src/main/java/com/faforever/client/chat/ChatMessageViewController.java index 4b3788412f..c22b433b99 100644 --- a/src/main/java/com/faforever/client/chat/ChatMessageViewController.java +++ b/src/main/java/com/faforever/client/chat/ChatMessageViewController.java @@ -56,8 +56,6 @@ @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public class ChatMessageViewController extends NodeController { - private static final String ACTION_PREFIX = "/me "; - private final ObjectFactory chatMessageItemCellFactory; private final NotificationService notificationService; private final ChatService chatService; @@ -69,8 +67,8 @@ public class ChatMessageViewController extends NodeController { public TextField messageTextField; public ListView messagesListView; public VBox root; - public Node emoticonsWindow; public Label typingLabel; + public Node emoticonsWindow; public EmoticonsWindowController emoticonsWindowController; private final List userMessageHistory = new ArrayList<>(); @@ -97,9 +95,6 @@ protected void onInitialize() { } })); - filteredMessages.subscribe( - () -> fxApplicationThreadExecutor.execute(() -> messagesListView.scrollTo(filteredMessages.size()))); - messagesListView.setSelectionModel(null); messagesListView.setItems(filteredMessages); messagesListView.setOrientation(Orientation.VERTICAL); @@ -128,11 +123,18 @@ protected void onInitialize() { } })); - emoticonsWindowController.setTextInputControl(messageTextField); + emoticonsWindowController.setOnEmoticonClicked(emoticon -> { + messageTextField.appendText(" " + emoticon.shortcodes().getFirst() + " "); + messageTextField.requestFocus(); + messageTextField.selectEnd(); + }); emoticonsPopup = PopupUtil.createPopup(AnchorLocation.WINDOW_BOTTOM_RIGHT, emoticonsWindow); emoticonsPopup.setConsumeAutoHidingEvents(false); createAutoCompletionHelper().bindTo(messageTextField); + + filteredMessages.subscribe( + () -> fxApplicationThreadExecutor.execute(() -> messagesListView.scrollTo(filteredMessages.size()))); } private AutoCompletionHelper createAutoCompletionHelper() { diff --git a/src/main/java/com/faforever/client/chat/ChatService.java b/src/main/java/com/faforever/client/chat/ChatService.java index 2dd448df48..87571297e6 100644 --- a/src/main/java/com/faforever/client/chat/ChatService.java +++ b/src/main/java/com/faforever/client/chat/ChatService.java @@ -1,5 +1,6 @@ package com.faforever.client.chat; +import com.faforever.client.chat.emoticons.Emoticon; import com.faforever.client.net.ConnectionState; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.collections.MapChangeListener; @@ -20,6 +21,10 @@ public interface ChatService { void disconnect(); + CompletableFuture reactToMessageInBackground(ChatMessage targetMessage, Emoticon reaction); + + CompletableFuture sendReplyInBackground(ChatMessage targetMessage, String message); + CompletableFuture sendMessageInBackground(ChatChannel chatChannel, String message); boolean userExistsInAnyChannel(String username); @@ -52,7 +57,7 @@ default void leaveChannel(String channelName) { void setChannelTopic(ChatChannel chatChannel, String text); - void onInitiatePrivateChat(String username); + void joinPrivateChat(String username); Set getChannels(); diff --git a/src/main/java/com/faforever/client/chat/ChatUserItemController.java b/src/main/java/com/faforever/client/chat/ChatUserItemController.java index 6bb2fd6525..905b79e840 100644 --- a/src/main/java/com/faforever/client/chat/ChatUserItemController.java +++ b/src/main/java/com/faforever/client/chat/ChatUserItemController.java @@ -186,7 +186,7 @@ public void onContextMenuRequested(ContextMenuEvent event) { public void onItemClicked(MouseEvent mouseEvent) { ChatChannelUser chatChannelUser = chatUser.get(); if (chatChannelUser != null && mouseEvent.getButton() == MouseButton.PRIMARY && mouseEvent.getClickCount() == 2) { - chatService.onInitiatePrivateChat(chatChannelUser.getUsername()); + chatService.joinPrivateChat(chatChannelUser.getUsername()); } } diff --git a/src/main/java/com/faforever/client/chat/KittehChatService.java b/src/main/java/com/faforever/client/chat/KittehChatService.java index 79128c10ab..33eabb3de2 100644 --- a/src/main/java/com/faforever/client/chat/KittehChatService.java +++ b/src/main/java/com/faforever/client/chat/KittehChatService.java @@ -2,6 +2,8 @@ import com.faforever.client.audio.AudioService; import com.faforever.client.chat.ChatMessage.Type; +import com.faforever.client.chat.emoticons.Emoticon; +import com.faforever.client.chat.emoticons.EmoticonService; import com.faforever.client.chat.kitteh.WhoAwayListener; import com.faforever.client.chat.kitteh.WhoAwayListener.WhoAwayMessageEvent; import com.faforever.client.config.ClientProperties; @@ -47,6 +49,7 @@ import org.kitteh.irc.client.library.defaults.listener.DefaultTagmsgListener; import org.kitteh.irc.client.library.element.Actor; import org.kitteh.irc.client.library.element.Channel; +import org.kitteh.irc.client.library.element.MessageTag; import org.kitteh.irc.client.library.element.MessageTag.Label; import org.kitteh.irc.client.library.element.MessageTag.MsgId; import org.kitteh.irc.client.library.element.MessageTag.Time; @@ -130,6 +133,7 @@ public class KittehChatService implements ChatService, InitializingBean, Disposa private final NotificationPrefs notificationPrefs; private final AudioService audioService; private final NotificationService notificationService; + private final EmoticonService emoticonService; private final NavigationHandler navigationHandler; private final TaskScheduler taskScheduler; @Qualifier("userWebClient") @@ -254,11 +258,22 @@ public void onTagMessage(PrivateTagMessageEvent event) { @Handler public void onTagMessage(ChannelTagMessageEvent event) { - event.getTag("+typing", Typing.class).ifPresent(typing -> { - if (event.getActor() instanceof User user) { - updateUserTypingState(typing.getState(), getOrCreateChatUser(user, event.getChannel())); - } - }); + if (event.getActor() instanceof User user) { + Channel channel = event.getChannel(); + ChatChannelUser chatUser = getOrCreateChatUser(user, channel); + + event.getTag("+typing", Typing.class).ifPresent(typing -> updateUserTypingState(typing.getState(), chatUser)); + + event.getTag("+draft/react") + .flatMap(MessageTag::getValue) + .map(emoticonService::getEmoticonByShortcode) + .ifPresent(reaction -> event.getTag("+draft/reply") + .flatMap(MessageTag::getValue) + .flatMap(targetId -> chatUser.getChannel().getMessage(targetId)) + .ifPresent(chatMessage -> fxApplicationThreadExecutor.execute( + () -> chatMessage.addReaction(reaction, chatUser)))); + } + } @VisibleForTesting @@ -330,13 +345,18 @@ public void onConnect(ClientNegotiationCompleteEvent event) { joinChannel(NEWBIE_CHANNEL_NAME); } - client.commands().capabilityRequest().enable(ECHO_MESSAGE).execute(); + client.commands() + .capabilityRequest() + .enable(ECHO_MESSAGE) + .enable("draft/chathistory") + .enable("draft/event-playback") + .execute(); } @Handler - private void onJoinEvent(ChannelJoinEvent event) { + public void onJoinEvent(ChannelJoinEvent event) { User user = event.getActor(); - getOrCreateChatUser(user, event.getChannel()); + updateChatUser(user, event.getChannel()); } @Handler @@ -354,20 +374,21 @@ public void onWhoAway(WhoAwayMessageEvent event) { } @Handler - private void onPartEvent(ChannelPartEvent event) { + public void onPartEvent(ChannelPartEvent event) { User user = event.getActor(); onChatUserLeftChannel(event.getChannel().getName(), user.getNick()); } @Handler - private void onChatUserQuit(UserQuitEvent event) { + public void onChatUserQuit(UserQuitEvent event) { User user = event.getUser(); String username = user.getNick(); - channels.values().forEach(channel -> onChatUserLeftChannel(channel.getName(), username)); + + List.copyOf(channels.keySet()).forEach(channelName -> onChatUserLeftChannel(channelName, username)); } @Handler - private void onTopicChange(ChannelTopicEvent event) { + public void onTopicChange(ChannelTopicEvent event) { String author = event.getNewTopic() .getSetter() .map(Actor::getName) @@ -379,7 +400,7 @@ private void onTopicChange(ChannelTopicEvent event) { } @Handler - private void onChannelMessage(ChannelMessageEvent event) { + public void onChannelMessage(ChannelMessageEvent event) { User user = event.getActor(); String channelName = event.getChannel().getName(); @@ -397,14 +418,23 @@ private void onChannelMessage(ChannelMessageEvent event) { .map(MsgId::getId) .orElseThrow( () -> new IllegalArgumentException("Message does not have an id: %s".formatted(event))); + ChatMessage targetMessage = event.getTag("+draft/reply") + .flatMap(MessageTag::getValue) + .flatMap(chatChannel::getMessage) + .orElse(null); + event.getTag("label", Label.class).map(Label::getLabel).ifPresent(chatChannel::removePendingMessage); - ChatMessage message = new ChatMessage(messageId, messageTime, sender, text, Type.MESSAGE); + ChatMessage message = new ChatMessage(messageId, messageTime, sender, text, Type.MESSAGE, targetMessage); chatChannel.addMessage(message); notifyIfMentioned(message); } private void notifyIfMentioned(ChatMessage chatMessage) { + if (chatMessage.getTime().isBefore(Instant.now().minusSeconds(60))) { + return; + } + String text = chatMessage.getContent(); ChatChannelUser sender = chatMessage.getSender(); if (sender.getCategory() == ChatUserCategory.FOE) { @@ -435,6 +465,10 @@ private void notifyIfMentioned(ChatMessage chatMessage) { } private void notifyOnPrivateMessage(ChatMessage chatMessage) { + if (chatMessage.getTime().isBefore(Instant.now().minusSeconds(60))) { + return; + } + ChatChannelUser sender = chatMessage.getSender(); ChatChannel channel = sender.getChannel(); if (channel.isPrivateChannel() && !channel.isOpen()) { @@ -455,7 +489,7 @@ private void notifyOnPrivateMessage(ChatMessage chatMessage) { } @Handler - private void onChannelCTCP(ChannelCtcpEvent event) { + public void onChannelCTCP(ChannelCtcpEvent event) { User user = event.getActor(); String channelName = event.getChannel().getName(); @@ -474,13 +508,18 @@ private void onChannelCTCP(ChannelCtcpEvent event) { .map(MsgId::getId) .orElseThrow( () -> new IllegalArgumentException("Message does not have an id: %s".formatted(event))); + ChatMessage targetMessage = event.getTag("+draft/reply") + .flatMap(MessageTag::getValue) + .flatMap(chatChannel::getMessage) + .orElse(null); + event.getTag("label", Label.class).map(Label::getLabel).ifPresent(chatChannel::removePendingMessage); - chatChannel.addMessage(new ChatMessage(messageId, messageTime, sender, message, Type.ACTION)); + chatChannel.addMessage(new ChatMessage(messageId, messageTime, sender, message, Type.ACTION, targetMessage)); } @Handler - private void onChannelModeChanged(ChannelModeEvent event) { + public void onChannelModeChanged(ChannelModeEvent event) { event.getStatusList().getAll().forEach(channelModeStatus -> channelModeStatus.getParameter().ifPresent(username -> { Mode changedMode = channelModeStatus.getMode(); Action modeAction = channelModeStatus.getAction(); @@ -497,7 +536,7 @@ private void onChannelModeChanged(ChannelModeEvent event) { } @Handler - private void onPrivateMessage(PrivateMessageEvent event) { + public void onPrivateMessage(PrivateMessageEvent event) { User user = event.getActor(); String senderNick = user.getNick(); @@ -519,9 +558,15 @@ private void onPrivateMessage(PrivateMessageEvent event) { .map(MsgId::getId) .orElseThrow( () -> new IllegalArgumentException("Message does not have an id: %s".formatted(event))); + + ChatMessage targetMessage = event.getTag("+draft/reply") + .flatMap(MessageTag::getValue) + .flatMap(chatChannel::getMessage) + .orElse(null); + event.getTag("label", Label.class).map(Label::getLabel).ifPresent(chatChannel::removePendingMessage); - ChatMessage message = new ChatMessage(messageId, messageTime, sender, text, Type.PENDING); + ChatMessage message = new ChatMessage(messageId, messageTime, sender, text, Type.MESSAGE, targetMessage); chatChannel.addMessage(message); notifyOnPrivateMessage(message); } @@ -554,10 +599,6 @@ private void onChatUserLeftChannel(String channelName, String username) { } chatChannel.removeUser(username); - - if (client.getNick().equalsIgnoreCase(username)) { - removeChannel(channelName); - } } private void onMessage(String message) { @@ -569,7 +610,7 @@ private void onException(Throwable throwable) { } @Handler - private void onDisconnect(ClientConnectionEndedEvent event) { + public void onDisconnect(ClientConnectionEndedEvent event) { client.getEventManager().unregisterEventListener(this); channels.values().forEach(ChatChannel::clearUsers); List.copyOf(channels.keySet()).forEach(this::removeChannel); @@ -579,7 +620,7 @@ private void onDisconnect(ClientConnectionEndedEvent event) { } @Handler - private void onFailedConnect(ClientConnectionFailedEvent event) { + public void onFailedConnect(ClientConnectionFailedEvent event) { connectionState.set(ConnectionState.DISCONNECTED); client.shutdown(); event.getCause().ifPresent(throwable -> log.error("Chat disconnected with cause", throwable)); @@ -646,10 +687,11 @@ public void connect() { .management() .eventListeners(eventListenerSuppliers) .then() - .build(); client.getMessageTagManager().registerTagCreator(MESSAGE_TAGS, "+typing", DefaultMessageTagTyping.FUNCTION); + client.getActorTracker().setQueryChannelInformation(false); + client.getEventManager().registerEventListener(this); userWebClientFactory.getObject() .get() @@ -661,7 +703,6 @@ public void connect() { client.getAuthManager() .addProtocol( new SaslPlain(client, "%s@FAF".formatted(username), "token:%s".formatted(token))); - client.getEventManager().registerEventListener(this); client.connect(); }); } @@ -674,6 +715,36 @@ public void disconnect() { } } + @Override + public CompletableFuture reactToMessageInBackground(ChatMessage targetMessage, Emoticon reaction) { + return CompletableFuture.runAsync(() -> { + new TagMessageCommand(client).target(targetMessage.getSender().getChannel().getName()) + .tags() + .add("+draft/reply", targetMessage.getId()) + .add("+draft/react", reaction.shortcodes().getFirst()) + .then() + .execute(); + }); + } + + @Override + public CompletableFuture sendReplyInBackground(ChatMessage targetMessage, String message) { + ChatChannel chatChannel = targetMessage.getSender().getChannel(); + String channelName = chatChannel.getName(); + ChatChannelUser sender = getOrCreateChatUser(getCurrentUsername(), channelName); + String id = String.valueOf(Objects.hash(new Random().nextInt(), message)); + return CompletableFuture.runAsync(() -> { + new MessageCommand(client).target(channelName) + .message(message) + .tags() + .add("label", id) + .add("+draft/reply", targetMessage.getId()) + .then() + .execute(); + chatChannel.addMessage(new ChatMessage(id, Instant.now(), sender, message, Type.PENDING, targetMessage)); + }); + } + @Override public CompletableFuture sendMessageInBackground(ChatChannel chatChannel, String message) { ChatChannelUser sender = getOrCreateChatUser(getCurrentUsername(), chatChannel.getName()); @@ -685,7 +756,7 @@ public CompletableFuture sendMessageInBackground(ChatChannel chatChannel, .add("label", id) .then() .execute(); - chatChannel.addMessage(new ChatMessage(id, Instant.now(), sender, message, Type.PENDING)); + chatChannel.addMessage(new ChatMessage(id, Instant.now(), sender, message, Type.PENDING, null)); }); } @@ -716,9 +787,10 @@ public void removeChannelsListener(MapChangeListener listen public void leaveChannel(ChatChannel channel) { if (!channel.isPrivateChannel()) { client.removeChannel(channel.getName()); - } else { - removeChannel(channel.getName()); } + + removeChannel(channel.getName()); + } private void removeChannel(String channelName) { @@ -737,6 +809,8 @@ public void joinChannel(String channelName) { bufferedChannels.add(channelName); } else { client.addChannel(channelName); + client.sendRawLine("CHATHISTORY LATEST " + channelName + " * " + chatPrefs.getMaxMessages() + 50); + client.sendRawLine("WHO " + channelName); } } @@ -795,8 +869,9 @@ private void incrementUnreadMessagesCount(Number oldValue, Number newValue) { } @Override - public void onInitiatePrivateChat(String username) { + public void joinPrivateChat(String username) { getOrCreateChannel(username); + client.sendRawLine("CHATHISTORY LATEST " + username + " * " + chatPrefs.getMaxMessages() + 50); } @Override diff --git a/src/main/java/com/faforever/client/chat/ReactionController.java b/src/main/java/com/faforever/client/chat/ReactionController.java new file mode 100644 index 0000000000..d7d62bd32b --- /dev/null +++ b/src/main/java/com/faforever/client/chat/ReactionController.java @@ -0,0 +1,144 @@ +package com.faforever.client.chat; + +import com.faforever.client.chat.emoticons.Emoticon; +import com.faforever.client.chat.emoticons.EmoticonService; +import com.faforever.client.fx.FxApplicationThreadExecutor; +import com.faforever.client.fx.NodeController; +import javafx.beans.binding.Bindings; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableSet; +import javafx.collections.SetChangeListener; +import javafx.event.EventHandler; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.HBox; +import javafx.scene.text.Font; +import javafx.util.Duration; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.SequencedCollection; +import java.util.Set; +import java.util.function.Consumer; + +@RequiredArgsConstructor +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class ReactionController extends NodeController { + + private static final String MY_REACTION_CLASS = "my-reaction"; + + private final EmoticonService emoticonService; + private final ChatService chatService; + private final FxApplicationThreadExecutor fxApplicationThreadExecutor; + + public Label label; + public HBox root; + public ImageView emoticonImageView; + + private final ObjectProperty reaction = new SimpleObjectProperty<>(); + private final ObjectProperty> reactors = new SimpleObjectProperty<>(); + private final ObjectProperty> onReactionClicked = new SimpleObjectProperty<>(); + + private final SetChangeListener reactorsListener = this::onReactorsChanged; + + private final Tooltip reactorsTooltip = new Tooltip(); + + @Override + public void onInitialize() { + root.onMouseClickedProperty() + .bind(onReactionClicked.flatMap(onReactionClicked -> reaction.map( + reaction -> (EventHandler) event -> onReactionClicked.accept(reaction))).when(showing)); + + emoticonImageView.imageProperty() + .bind(reaction.map(Emoticon::shortcodes) + .map(SequencedCollection::getFirst) + .map(emoticonService::getImageByShortcode) + .when(showing)); + label.textProperty().bind(reactors.flatMap(Bindings::size).map(String::valueOf).when(showing)); + + reactorsTooltip.textProperty() + .bind(reactors.flatMap( + reactors -> Bindings.createStringBinding(() -> String.join(", ", reactors), reactors)) + .when(showing)); + + reactors.subscribe((oldValue, newValue) -> { + if (oldValue != null) { + oldValue.removeListener(reactorsListener); + } + + if (newValue != null) { + updateSelected(newValue); + newValue.addListener(reactorsListener); + } + }); + + reactorsTooltip.setFont(new Font(14)); + reactorsTooltip.setShowDuration(Duration.seconds(10)); + reactorsTooltip.setShowDelay(Duration.ZERO); + reactorsTooltip.setHideDelay(Duration.ZERO); + Tooltip.install(root, reactorsTooltip); + } + + private void onReactorsChanged(SetChangeListener.Change change) { + ObservableSet set = change.getSet(); + updateSelected(set); + } + + private void updateSelected(Set set) { + boolean selected = set.contains(chatService.getCurrentUsername()); + fxApplicationThreadExecutor.execute(() -> { + if (selected) { + root.getStyleClass().add(MY_REACTION_CLASS); + } else { + root.getStyleClass().remove(MY_REACTION_CLASS); + } + }); + } + + @Override + public HBox getRoot() { + return root; + } + + public Emoticon getReaction() { + return reaction.get(); + } + + public ObjectProperty reactionProperty() { + return reaction; + } + + public void setReaction(Emoticon reaction) { + this.reaction.set(reaction); + } + + public ObservableSet getReactors() { + return reactors.get(); + } + + public ObjectProperty> reactorsProperty() { + return reactors; + } + + public void setReactors(ObservableSet reactors) { + this.reactors.set(reactors); + } + + public Consumer getOnReactionClicked() { + return onReactionClicked.get(); + } + + public ObjectProperty> onReactionClickedProperty() { + return onReactionClicked; + } + + public void setOnReactionClicked(Consumer onReactionClicked) { + this.onReactionClicked.set(onReactionClicked); + } +} diff --git a/src/main/java/com/faforever/client/chat/emoticons/Emoticon.java b/src/main/java/com/faforever/client/chat/emoticons/Emoticon.java index a95757aea8..6071269c94 100644 --- a/src/main/java/com/faforever/client/chat/emoticons/Emoticon.java +++ b/src/main/java/com/faforever/client/chat/emoticons/Emoticon.java @@ -1,7 +1,5 @@ package com.faforever.client.chat.emoticons; -import javafx.scene.image.Image; - import java.util.List; -public record Emoticon(List shortcodes, String base64SvgContent, Image image) {} +public record Emoticon(List shortcodes, String base64SvgContent) {} diff --git a/src/main/java/com/faforever/client/chat/emoticons/EmoticonController.java b/src/main/java/com/faforever/client/chat/emoticons/EmoticonController.java index ac7a7650bb..058f8429f2 100644 --- a/src/main/java/com/faforever/client/chat/emoticons/EmoticonController.java +++ b/src/main/java/com/faforever/client/chat/emoticons/EmoticonController.java @@ -1,8 +1,15 @@ package com.faforever.client.chat.emoticons; import com.faforever.client.fx.NodeController; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.event.EventHandler; +import javafx.geometry.Insets; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.AnchorPane; import javafx.scene.text.Font; import javafx.util.Duration; @@ -23,26 +30,91 @@ public class EmoticonController extends NodeController { private final EmoticonService emoticonService; - private final Font shortcodesFont = new Font(14d); - public AnchorPane root; public ImageView emoticonImageView; - public void setEmoticon(Emoticon emoticon, Consumer onAction) { - emoticonImageView.setImage(emoticonService.getImageByShortcode(emoticon.shortcodes().getFirst())); - root.setOnMouseClicked(event -> onAction.accept(emoticon.shortcodes().getFirst())); + private final Tooltip shortCodesTooltip = new Tooltip(); + private final ObjectProperty> onEmoticonClicked = new SimpleObjectProperty<>(); + private final ObjectProperty emoticon = new SimpleObjectProperty<>(); + private final IntegerProperty emoticonSize = new SimpleIntegerProperty(36); + private final ObjectProperty emoticonPadding = new SimpleObjectProperty<>(new Insets(5, 5, 5, 5)); + + @Override + protected void onInitialize() { + root.onMouseClickedProperty() + .bind(onEmoticonClicked.flatMap(onEmoticonClicked -> emoticon.map( + emoticon -> (EventHandler) event -> onEmoticonClicked.accept(emoticon))).when(showing)); + emoticonImageView.imageProperty() + .bind(emoticon.map(Emoticon::shortcodes) + .map(List::getFirst) + .map(emoticonService::getImageByShortcode) + .when(showing)); + emoticonImageView.fitHeightProperty().bind(emoticonSize); + emoticonImageView.fitWidthProperty().bind(emoticonSize); + root.paddingProperty().bind(emoticonPadding); + + shortCodesTooltip.textProperty() + .bind(emoticon.map(Emoticon::shortcodes) + .map(shortCodes -> String.join("\t", shortCodes)) + .when(showing)); + + shortCodesTooltip.setFont(new Font(14d)); + shortCodesTooltip.setShowDuration(Duration.seconds(10)); + shortCodesTooltip.setShowDelay(Duration.ZERO); + shortCodesTooltip.setHideDelay(Duration.ZERO); + Tooltip.install(root, shortCodesTooltip); + } + + public void setEmoticon(String shortcode) { + setEmoticon(emoticonService.getEmoticonByShortcode(shortcode)); + } + + public void setEmoticon(Emoticon emoticon) { + this.emoticon.set(emoticon); + } + + public Emoticon getEmoticon() { + return emoticon.get(); + } + + public ObjectProperty emoticonProperty() { + return emoticon; + } + + public void setOnEmoticonClicked(Consumer onEmoticonClicked) { + this.onEmoticonClicked.set(onEmoticonClicked); + } + + public Consumer getOnEmoticonClicked() { + return onEmoticonClicked.get(); + } + + public ObjectProperty> onEmoticonClickedProperty() { + return onEmoticonClicked; + } + + public int getEmoticonSize() { + return emoticonSize.get(); + } + + public IntegerProperty emoticonSizeProperty() { + return emoticonSize; + } + + public void setEmoticonSize(int emoticonSize) { + this.emoticonSize.set(emoticonSize); + } + + public Insets getEmoticonPadding() { + return emoticonPadding.get(); + } - displayShortcodesOnHover(emoticon.shortcodes()); + public ObjectProperty emoticonPaddingProperty() { + return emoticonPadding; } - private void displayShortcodesOnHover(List shortcodes) { - Tooltip tooltip = new Tooltip(); - tooltip.setText(String.join("\t",shortcodes)); - tooltip.setFont(shortcodesFont); - tooltip.setShowDuration(Duration.seconds(10)); - tooltip.setShowDelay(Duration.ZERO); - tooltip.setHideDelay(Duration.ZERO); - Tooltip.install(root, tooltip); + public void setEmoticonPadding(Insets emoticonPadding) { + this.emoticonPadding.set(emoticonPadding); } @Override diff --git a/src/main/java/com/faforever/client/chat/emoticons/EmoticonService.java b/src/main/java/com/faforever/client/chat/emoticons/EmoticonService.java index d1cf33e5f3..8207f7f76f 100644 --- a/src/main/java/com/faforever/client/chat/emoticons/EmoticonService.java +++ b/src/main/java/com/faforever/client/chat/emoticons/EmoticonService.java @@ -14,15 +14,12 @@ import java.io.IOException; import java.io.InputStream; -import java.util.Arrays; import java.util.Base64; import java.util.Base64.Decoder; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.regex.Pattern; -import java.util.stream.Collectors; @Slf4j @Service @@ -35,11 +32,10 @@ public class EmoticonService implements InitializingBean { private final ObjectMapper objectMapper; private List emoticonsGroups; - private Pattern emoticonShortcodeDetectorPattern; private final Decoder decoder = Base64.getDecoder(); - private final Map shortcodeToBase64SvgContent = new HashMap<>(); private final Map shortcodeToImage = new HashMap<>(); + private final Map shortcodeToEmoticon = new HashMap<>(); @Override public void afterPropertiesSet() { @@ -49,22 +45,16 @@ public void afterPropertiesSet() { @VisibleForTesting void loadAndVerifyEmoticons() { try (InputStream emoticonsInputStream = EMOTICONS_JSON_FILE_RESOURCE.getInputStream()) { - emoticonsGroups = Arrays.asList(objectMapper.readValue(emoticonsInputStream, EmoticonsGroup[].class)); + emoticonsGroups = List.of(objectMapper.readValue(emoticonsInputStream, EmoticonsGroup[].class)); emoticonsGroups.stream().flatMap(emoticonsGroup -> emoticonsGroup.emoticons().stream()).forEach(emoticon -> { String base64SvgContent = emoticon.base64SvgContent(); Image image = new Image(IOUtils.toInputStream(new String(decoder.decode(base64SvgContent)))); emoticon.shortcodes().forEach(shortcode -> { - if (shortcodeToBase64SvgContent.containsKey(shortcode)) { + if (shortcodeToImage.put(shortcode, image) != null || shortcodeToEmoticon.put(shortcode, emoticon) != null) { throw new ProgrammingError("Shortcode `" + shortcode + "` is already taken"); } - shortcodeToBase64SvgContent.put(shortcode, base64SvgContent); - shortcodeToImage.put(shortcode, image); }); }); - emoticonShortcodeDetectorPattern = Pattern.compile(shortcodeToBase64SvgContent.keySet() - .stream() - .map(Pattern::quote) - .collect(Collectors.joining("|"))); } catch (IOException e) { throw new AssetLoadException("Unable to load emoticons", e, ""); } @@ -74,15 +64,16 @@ public List getEmoticonsGroups() { return emoticonsGroups; } - public Pattern getEmoticonShortcodeDetectorPattern() { - return emoticonShortcodeDetectorPattern; - } - - public String getBase64SvgContentByShortcode(String shortcode) { - return shortcodeToBase64SvgContent.get(shortcode); + public boolean isEmoticonShortcode(String shortcode) { + return shortcodeToEmoticon.containsKey(shortcode); } public Image getImageByShortcode(String shortcode) { return shortcodeToImage.get(shortcode); } + + public Emoticon getEmoticonByShortcode(String shortcode) { + return shortcodeToEmoticon.get(shortcode); + } + } diff --git a/src/main/java/com/faforever/client/chat/emoticons/EmoticonsGroup.java b/src/main/java/com/faforever/client/chat/emoticons/EmoticonsGroup.java index 6f148d7d56..53e0ed14b2 100644 --- a/src/main/java/com/faforever/client/chat/emoticons/EmoticonsGroup.java +++ b/src/main/java/com/faforever/client/chat/emoticons/EmoticonsGroup.java @@ -2,4 +2,4 @@ import java.util.List; -public record EmoticonsGroup(String name, String attribution, List emoticons) {} +public record EmoticonsGroup(String name, String attributionUrl, List emoticons) {} diff --git a/src/main/java/com/faforever/client/chat/emoticons/EmoticonsGroupController.java b/src/main/java/com/faforever/client/chat/emoticons/EmoticonsGroupController.java index 1d7b3e1da0..77a0b81445 100644 --- a/src/main/java/com/faforever/client/chat/emoticons/EmoticonsGroupController.java +++ b/src/main/java/com/faforever/client/chat/emoticons/EmoticonsGroupController.java @@ -1,9 +1,15 @@ package com.faforever.client.chat.emoticons; +import com.faforever.client.fx.FxApplicationThreadExecutor; import com.faforever.client.fx.JavaFxUtil; import com.faforever.client.fx.NodeController; import com.faforever.client.fx.PlatformService; import com.faforever.client.theme.UiService; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.layout.AnchorPane; @@ -12,14 +18,12 @@ import javafx.scene.layout.VBox; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import java.util.List; import java.util.function.Consumer; -import java.util.stream.Collectors; @Slf4j @Component @@ -29,6 +33,7 @@ public class EmoticonsGroupController extends NodeController { private final UiService uiService; private final PlatformService platformService; + private final FxApplicationThreadExecutor fxApplicationThreadExecutor; public VBox root; public Label groupLabel; @@ -36,25 +41,59 @@ public class EmoticonsGroupController extends NodeController { public Hyperlink attributionHyperlink; public FlowPane emoticonsPane; + private final ObjectProperty emoticonsGroup = new SimpleObjectProperty<>(); + private final ObjectProperty> onEmoticonClicked = new SimpleObjectProperty>(); + @Override protected void onInitialize() { JavaFxUtil.bindManagedToVisible(attributionPane); + ObservableValue attribution = emoticonsGroup.map(EmoticonsGroup::attributionUrl); + attributionHyperlink.textProperty().bind(attribution.when(showing)); + attributionHyperlink.onActionProperty() + .bind(attribution.map( + url -> (EventHandler) event -> platformService.showDocument(url)) + .when(showing)); + attributionPane.visibleProperty().bind(attributionHyperlink.textProperty().isNotEmpty().when(showing)); + groupLabel.textProperty().bind(emoticonsGroup.map(EmoticonsGroup::name)); + emoticonsGroup.subscribe(this::populateEmoticons); + } + + public EmoticonsGroup getEmoticonsGroup() { + return emoticonsGroup.get(); + } + + public ObjectProperty emoticonsGroupProperty() { + return emoticonsGroup; + } + + public void setEmoticonsGroup(EmoticonsGroup emoticonsGroup) { + this.emoticonsGroup.set(emoticonsGroup); + } + + public Consumer getOnEmoticonClicked() { + return onEmoticonClicked.get(); + } + + public ObjectProperty> onEmoticonClickedProperty() { + return onEmoticonClicked; + } + + public void setOnEmoticonClicked(Consumer onEmoticonClicked) { + this.onEmoticonClicked.set(onEmoticonClicked); } - public void setGroup(EmoticonsGroup group, Consumer onEmoticonAction) { - groupLabel.setText(group.name()); - String attribution = group.attribution(); - if (!StringUtils.isBlank(attribution)) { - attributionHyperlink.setText(attribution); - attributionHyperlink.setOnAction(event -> platformService.showDocument(attribution)); - attributionPane.setVisible(true); + private void populateEmoticons(EmoticonsGroup group) { + if (group == null) { + fxApplicationThreadExecutor.execute(() -> emoticonsPane.getChildren().clear()); + } else { + List emoticonViewList = group.emoticons().stream().map(emoticon -> { + EmoticonController controller = uiService.loadFxml("theme/chat/emoticons/emoticon.fxml"); + controller.setEmoticon(emoticon); + controller.onEmoticonClickedProperty().bind(onEmoticonClicked); + return controller.getRoot(); + }).toList(); + fxApplicationThreadExecutor.execute(() -> emoticonsPane.getChildren().setAll(emoticonViewList)); } - List emoticonViewList = group.emoticons().stream().map(emoticon -> { - EmoticonController controller = uiService.loadFxml("theme/chat/emoticons/emoticon.fxml"); - controller.setEmoticon(emoticon, onEmoticonAction); - return controller.getRoot(); - }).collect(Collectors.toList()); - emoticonsPane.getChildren().addAll(emoticonViewList); } @Override diff --git a/src/main/java/com/faforever/client/chat/emoticons/EmoticonsWindowController.java b/src/main/java/com/faforever/client/chat/emoticons/EmoticonsWindowController.java index fafb1794e6..5669cb3c40 100644 --- a/src/main/java/com/faforever/client/chat/emoticons/EmoticonsWindowController.java +++ b/src/main/java/com/faforever/client/chat/emoticons/EmoticonsWindowController.java @@ -2,12 +2,11 @@ import com.faforever.client.fx.NodeController; import com.faforever.client.theme.UiService; -import com.google.common.annotations.VisibleForTesting; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.scene.Node; -import javafx.scene.control.TextInputControl; import javafx.scene.layout.VBox; import lombok.RequiredArgsConstructor; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Scope; @@ -28,27 +27,30 @@ public class EmoticonsWindowController extends NodeController { public VBox root; - @Setter - private TextInputControl textInputControl; + private final ObjectProperty> onEmoticonClicked = new SimpleObjectProperty<>(); @Override protected void onInitialize() { List nodes = new ArrayList<>(); emoticonService.getEmoticonsGroups().forEach(group -> { EmoticonsGroupController controller = uiService.loadFxml("theme/chat/emoticons/emoticons_group.fxml"); - controller.setGroup(group, onEmoticonClicked()); + controller.setEmoticonsGroup(group); + controller.onEmoticonClickedProperty().bind(onEmoticonClicked); nodes.add(controller.getRoot()); }); root.getChildren().addAll(nodes); } - @VisibleForTesting - protected Consumer onEmoticonClicked() { - return shortcode -> { - textInputControl.appendText(" " + shortcode + " "); - textInputControl.requestFocus(); - textInputControl.selectEnd(); - }; + public Consumer getOnEmoticonClicked() { + return onEmoticonClicked.get(); + } + + public ObjectProperty> onEmoticonClickedProperty() { + return onEmoticonClicked; + } + + public void setOnEmoticonClicked(Consumer onEmoticonClicked) { + this.onEmoticonClicked.set(onEmoticonClicked); } @Override diff --git a/src/main/java/com/faforever/client/chat/kitteh/WhoAwayListener.java b/src/main/java/com/faforever/client/chat/kitteh/WhoAwayListener.java index 1211ce4151..04c4a70441 100644 --- a/src/main/java/com/faforever/client/chat/kitteh/WhoAwayListener.java +++ b/src/main/java/com/faforever/client/chat/kitteh/WhoAwayListener.java @@ -30,8 +30,8 @@ public void who(ClientReceiveNumericEvent event) { } final String channel = event.getParameters().get(1); - final String nick = event.getParameters().get(5); - final String status = event.getParameters().get(6); + final String nick = event.getParameters().get(5); + final String status = event.getParameters().get(6); boolean isAway = status.contains("G"); this.fire(new WhoAwayMessageEvent(this.getClient(), channel, nick, isAway)); diff --git a/src/main/java/com/faforever/client/fx/BrowserCallback.java b/src/main/java/com/faforever/client/fx/BrowserCallback.java index 3ff6e9fc52..8c52de624a 100644 --- a/src/main/java/com/faforever/client/fx/BrowserCallback.java +++ b/src/main/java/com/faforever/client/fx/BrowserCallback.java @@ -1,20 +1,8 @@ package com.faforever.client.fx; -import com.faforever.client.chat.ChatService; -import com.faforever.client.chat.UrlPreviewResolver; -import com.faforever.client.clan.ClanService; -import com.faforever.client.clan.ClanTooltipController; import com.faforever.client.config.ClientProperties; import com.faforever.client.main.event.ShowReplayEvent; import com.faforever.client.navigation.NavigationHandler; -import com.faforever.client.theme.UiService; -import com.faforever.client.ui.StageHolder; -import com.faforever.client.util.PopupUtil; -import com.google.common.annotations.VisibleForTesting; -import javafx.scene.control.ContentDisplay; -import javafx.scene.control.Tooltip; -import javafx.stage.Popup; -import javafx.stage.PopupWindow.AnchorLocation; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.ConfigurableBeanFactory; @@ -31,19 +19,9 @@ public class BrowserCallback implements InitializingBean { private final PlatformService platformService; - private final UrlPreviewResolver urlPreviewResolver; - private final ClanService clanService; - private final UiService uiService; private final ClientProperties clientProperties; - private final ChatService chatService; - private final FxApplicationThreadExecutor fxApplicationThreadExecutor; private final NavigationHandler navigationHandler; - @VisibleForTesting - Popup clanInfoPopup; - private Tooltip linkPreviewTooltip; - private double lastMouseX; - private double lastMouseY; private Pattern replayUrlPattern; @Override @@ -67,99 +45,4 @@ public void openUrl(String url) { String replayId = replayUrlMatcher.group(1); navigationHandler.navigateTo(new ShowReplayEvent(Integer.parseInt(replayId))); } - - /** - * Called from JavaScript when user clicked a channel link. - */ - @SuppressWarnings("unused") - public void openChannel(String channelName) { - chatService.joinChannel(channelName); - } - - /** - * Called from JavaScript when user clicks on user name in chat - */ - @SuppressWarnings("unused") - public void openPrivateMessageTab(String username) { - chatService.onInitiatePrivateChat(username); - } - - /** - * Called from JavaScript when user no longer hovers over an URL. - */ - @SuppressWarnings("unused") - public void hideUrlPreview() { - if (linkPreviewTooltip != null) { - linkPreviewTooltip.hide(); - linkPreviewTooltip = null; - } - } - - /** - * Called from JavaScript when user hovers over an URL. - */ - @SuppressWarnings("unused") - public void previewUrl(String urlString) { - urlPreviewResolver.resolvePreview(urlString).thenAccept(optionalPreview -> optionalPreview.ifPresent(preview -> { - linkPreviewTooltip = new Tooltip(preview.description()); - linkPreviewTooltip.setAutoHide(true); - linkPreviewTooltip.setAnchorLocation(AnchorLocation.CONTENT_BOTTOM_LEFT); - linkPreviewTooltip.setGraphic(preview.node()); - linkPreviewTooltip.setContentDisplay(ContentDisplay.TOP); - fxApplicationThreadExecutor.execute(() -> linkPreviewTooltip.show(StageHolder.getStage(), lastMouseX + 20, lastMouseY)); - })); - } - - /** - * Called from JavaScript when user hovers over a clan tag. - */ - @SuppressWarnings("unused") - public void showClanInfo(String clanTag) { - clanService.getClanByTag(clanTag).thenAcceptAsync(clan -> { - if (clan.isEmpty() || clanTag.isEmpty()) { - return; - } - ClanTooltipController clanTooltipController = uiService.loadFxml("theme/chat/clan_tooltip.fxml"); - clanTooltipController.setClan(clan.get()); - clanTooltipController.getRoot().getStyleClass().add("tooltip"); - - clanInfoPopup = PopupUtil.createPopup(AnchorLocation.CONTENT_TOP_LEFT, clanTooltipController.getRoot()); - clanInfoPopup.show(StageHolder.getStage(), lastMouseX, lastMouseY + 10); - }, fxApplicationThreadExecutor); - } - - /** - * Called from JavaScript when user no longer hovers over a clan tag. - */ - @SuppressWarnings("unused") - public void hideClanInfo() { - if (clanInfoPopup == null) { - return; - } - fxApplicationThreadExecutor.execute(() -> { - clanInfoPopup.hide(); - clanInfoPopup = null; - }); - } - - /** - * Called from JavaScript when user clicks on clan tag. - */ - @SuppressWarnings("unused") - public void showClanWebsite(String clanTag) { - clanService.getClanByTag(clanTag).thenAccept(clan -> { - if (clan.isEmpty()) { - return; - } - platformService.showDocument(clan.get().getWebsiteUrl()); - }); - } - - void setLastMouseX(double screenX) { - lastMouseX = screenX; - } - - void setLastMouseY(double screenY) { - lastMouseY = screenY; - } } diff --git a/src/main/java/com/faforever/client/fx/Controller.java b/src/main/java/com/faforever/client/fx/Controller.java index d2d088c2ed..62a3e1c9f1 100644 --- a/src/main/java/com/faforever/client/fx/Controller.java +++ b/src/main/java/com/faforever/client/fx/Controller.java @@ -6,7 +6,7 @@ import java.util.ArrayList; import java.util.List; -public sealed abstract class Controller permits MenuItemController, NodeController, TabController { +public abstract class Controller { protected BooleanExpression attached; protected BooleanExpression showing; diff --git a/src/main/java/com/faforever/client/fx/MenuItemController.java b/src/main/java/com/faforever/client/fx/MenuItemController.java index 7cae598679..c9f2a37f79 100644 --- a/src/main/java/com/faforever/client/fx/MenuItemController.java +++ b/src/main/java/com/faforever/client/fx/MenuItemController.java @@ -4,7 +4,7 @@ import javafx.scene.control.MenuItem; -public abstract non-sealed class MenuItemController extends Controller { +public abstract class MenuItemController extends Controller { @Override protected BooleanExpression createAttachedExpression() { diff --git a/src/main/java/com/faforever/client/fx/NodeController.java b/src/main/java/com/faforever/client/fx/NodeController.java index 667d67edca..a63c06cedc 100644 --- a/src/main/java/com/faforever/client/fx/NodeController.java +++ b/src/main/java/com/faforever/client/fx/NodeController.java @@ -5,7 +5,7 @@ import javafx.scene.Node; -public abstract non-sealed class NodeController extends Controller { +public abstract class NodeController extends Controller { @Override protected BooleanExpression createAttachedExpression() { diff --git a/src/main/java/com/faforever/client/fx/TabController.java b/src/main/java/com/faforever/client/fx/TabController.java index eb991c89b2..3942c46e80 100644 --- a/src/main/java/com/faforever/client/fx/TabController.java +++ b/src/main/java/com/faforever/client/fx/TabController.java @@ -4,7 +4,7 @@ import javafx.scene.control.Tab; -public abstract non-sealed class TabController extends Controller { +public abstract class TabController extends Controller { @Override protected BooleanExpression createAttachedExpression() { diff --git a/src/main/java/com/faforever/client/fx/WebViewConfigurer.java b/src/main/java/com/faforever/client/fx/WebViewConfigurer.java index 4236b6aa0a..bcb982f46d 100644 --- a/src/main/java/com/faforever/client/fx/WebViewConfigurer.java +++ b/src/main/java/com/faforever/client/fx/WebViewConfigurer.java @@ -3,9 +3,7 @@ import com.faforever.client.config.ClientProperties; import com.faforever.client.theme.ThemeService; import javafx.concurrent.Worker.State; -import javafx.event.EventHandler; import javafx.scene.input.KeyCode; -import javafx.scene.input.MouseEvent; import javafx.scene.paint.Color; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; @@ -50,11 +48,6 @@ public void configureWebView(WebView webView) { }); BrowserCallback browserCallback = browserCallbackFactory.getObject(); - EventHandler moveHandler = event -> { - browserCallback.setLastMouseX(event.getScreenX()); - browserCallback.setLastMouseY(event.getScreenY()); - }; - webView.addEventHandler(MouseEvent.MOUSE_MOVED, moveHandler); engine.setUserAgent(clientProperties.getUserAgent()); // removes faforever.com header and footer themeService.registerWebView(webView); diff --git a/src/main/java/com/faforever/client/fx/contextmenu/SendPrivateMessageClanLeaderMenuItem.java b/src/main/java/com/faforever/client/fx/contextmenu/SendPrivateMessageClanLeaderMenuItem.java index b2e8a622c5..e43635271b 100644 --- a/src/main/java/com/faforever/client/fx/contextmenu/SendPrivateMessageClanLeaderMenuItem.java +++ b/src/main/java/com/faforever/client/fx/contextmenu/SendPrivateMessageClanLeaderMenuItem.java @@ -26,8 +26,7 @@ protected void onClicked() { clanService.getClanByTag(object.getClan()) .thenAccept(possibleClan -> possibleClan.map(ClanBean::getLeader) - .map(PlayerBean::getUsername) - .ifPresent(chatService::onInitiatePrivateChat)); + .map(PlayerBean::getUsername).ifPresent(chatService::joinPrivateChat)); } @Override diff --git a/src/main/java/com/faforever/client/fx/contextmenu/SendPrivateMessageMenuItem.java b/src/main/java/com/faforever/client/fx/contextmenu/SendPrivateMessageMenuItem.java index f6d0f98e6c..428635773c 100644 --- a/src/main/java/com/faforever/client/fx/contextmenu/SendPrivateMessageMenuItem.java +++ b/src/main/java/com/faforever/client/fx/contextmenu/SendPrivateMessageMenuItem.java @@ -22,7 +22,7 @@ public class SendPrivateMessageMenuItem extends AbstractMenuItem { @Override protected void onClicked() { Assert.isTrue(!StringUtils.isBlank(object), "No username has been set"); - chatService.onInitiatePrivateChat(object); + chatService.joinPrivateChat(object); } @Override diff --git a/src/main/java/com/faforever/client/player/FriendOnlineNotifier.java b/src/main/java/com/faforever/client/player/FriendOnlineNotifier.java index e94dd93785..6c8aa6cc48 100644 --- a/src/main/java/com/faforever/client/player/FriendOnlineNotifier.java +++ b/src/main/java/com/faforever/client/player/FriendOnlineNotifier.java @@ -42,8 +42,7 @@ void onPlayerOnline(PlayerBean player) { new TransientNotification( i18n.get("friend.nowOnlineNotification.title", player.getUsername()), i18n.get("friend.nowOnlineNotification.action"), - IdenticonUtil.createIdenticon(player.getId()), - () -> chatService.onInitiatePrivateChat(player.getUsername()) + IdenticonUtil.createIdenticon(player.getId()), () -> chatService.joinPrivateChat(player.getUsername()) )); } } diff --git a/src/main/java/com/faforever/client/replay/WatchButtonController.java b/src/main/java/com/faforever/client/replay/WatchButtonController.java index 5ee9d100ba..adec99143f 100644 --- a/src/main/java/com/faforever/client/replay/WatchButtonController.java +++ b/src/main/java/com/faforever/client/replay/WatchButtonController.java @@ -118,7 +118,6 @@ public ObjectProperty gameProperty() { } private void showContextMenu() { - Bounds screenBounds = watchButton.localToScreen(watchButton.getBoundsInLocal()); GameBean gameBean = getGame(); if (gameBean == null) { return; @@ -130,6 +129,7 @@ private void showContextMenu() { .addItem(RunReplayImmediatelyMenuItem.class, gameBean) .addItem(CancelActionRunReplayImmediatelyMenuItem.class, gameBean) .build(); + Bounds screenBounds = watchButton.localToScreen(watchButton.getBoundsInLocal()); contextMenu.show(watchButton.getScene().getWindow(), screenBounds.getMinX(), screenBounds.getMaxY()); } diff --git a/src/main/java/com/faforever/client/theme/ThemeService.java b/src/main/java/com/faforever/client/theme/ThemeService.java index c7371a20da..eb96057507 100644 --- a/src/main/java/com/faforever/client/theme/ThemeService.java +++ b/src/main/java/com/faforever/client/theme/ThemeService.java @@ -93,11 +93,6 @@ public class ThemeService implements InitializingBean, DisposableBean { public static final String DEFAULT_ACHIEVEMENT_IMAGE = "theme/images/default_achievement.png"; public static final String MENTION_SOUND = "theme/sounds/userMentionSound.mp3"; public static final String CSS_CLASS_ICON = "icon"; - public static final String CHAT_CONTAINER = "theme/chat/chat_container.html"; - public static final String CHAT_SECTION_EXTENDED = "theme/chat/extended/chat_section.html"; - public static final String CHAT_SECTION_COMPACT = "theme/chat/compact/chat_section.html"; - public static final String CHAT_TEXT_EXTENDED = "theme/chat/extended/chat_text.html"; - public static final String CHAT_TEXT_COMPACT = "theme/chat/compact/chat_text.html"; public static final String CHAT_LIST_STATUS_HOSTING = "theme/images/player_status/host.png"; public static final String CHAT_LIST_STATUS_LOBBYING = "theme/images/player_status/lobby.png"; public static final String CHAT_LIST_STATUS_PLAYING = "theme/images/player_status/playing.png"; diff --git a/src/main/resources/images/emoticons/emoticons.json b/src/main/resources/images/emoticons/emoticons.json index 80743a10fd..cc82cdfd76 100644 --- a/src/main/resources/images/emoticons/emoticons.json +++ b/src/main/resources/images/emoticons/emoticons.json @@ -1,7 +1,6 @@ [ { "name": "FAF", - "attribution": "", "emoticons": [ { "shortcodes": [ @@ -31,7 +30,7 @@ }, { "name": "Common", - "attribution": "https://twemoji.twitter.com/", + "attributionUrl": "https://twemoji.twitter.com/", "emoticons": [ { "shortcodes": [ diff --git a/src/main/resources/theme/chat/chat_container.html b/src/main/resources/theme/chat/chat_container.html deleted file mode 100644 index 9553eeebe1..0000000000 --- a/src/main/resources/theme/chat/chat_container.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - -
- -
- -
-
- - diff --git a/src/main/resources/theme/chat/chat_message.fxml b/src/main/resources/theme/chat/chat_message.fxml index a2e56b93ea..e4d15342ad 100644 --- a/src/main/resources/theme/chat/chat_message.fxml +++ b/src/main/resources/theme/chat/chat_message.fxml @@ -1,15 +1,18 @@ + + + + fx:id="root" styleClass="message-container" onMouseEntered="#onMouseEntered" onMouseExited="#onMouseExited"> @@ -21,6 +24,23 @@ + + + + + diff --git a/src/main/resources/theme/chat/compact/chat_section.html b/src/main/resources/theme/chat/compact/chat_section.html deleted file mode 100644 index f50ebf85d5..0000000000 --- a/src/main/resources/theme/chat/compact/chat_section.html +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - -
- - - - - - {decorated-clan-tag} - - - - {username} - - - - - {time}
diff --git a/src/main/resources/theme/chat/compact/chat_text.html b/src/main/resources/theme/chat/compact/chat_text.html deleted file mode 100644 index fe22f5d0ca..0000000000 --- a/src/main/resources/theme/chat/compact/chat_text.html +++ /dev/null @@ -1 +0,0 @@ -{text} diff --git a/src/main/resources/theme/chat/emoticons/emoticon.fxml b/src/main/resources/theme/chat/emoticons/emoticon.fxml index 5068bae38e..551acd498f 100644 --- a/src/main/resources/theme/chat/emoticons/emoticon.fxml +++ b/src/main/resources/theme/chat/emoticons/emoticon.fxml @@ -1,14 +1,13 @@ - - @@ -16,7 +15,4 @@ - - - diff --git a/src/main/resources/theme/chat/emoticons/reaction.fxml b/src/main/resources/theme/chat/emoticons/reaction.fxml new file mode 100644 index 0000000000..f304539d16 --- /dev/null +++ b/src/main/resources/theme/chat/emoticons/reaction.fxml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/theme/chat/extended/chat_section.html b/src/main/resources/theme/chat/extended/chat_section.html deleted file mode 100644 index abd0860c92..0000000000 --- a/src/main/resources/theme/chat/extended/chat_section.html +++ /dev/null @@ -1,26 +0,0 @@ -
-
- - -
- - {decorated-clan-tag} - - - - {username} - - - {time} -
-
- -
diff --git a/src/main/resources/theme/chat/extended/chat_text.html b/src/main/resources/theme/chat/extended/chat_text.html deleted file mode 100644 index 89e414d373..0000000000 --- a/src/main/resources/theme/chat/extended/chat_text.html +++ /dev/null @@ -1 +0,0 @@ -
{text}
diff --git a/src/main/resources/theme/icons.css b/src/main/resources/theme/icons.css index 2d9973b42b..76cf502111 100644 --- a/src/main/resources/theme/icons.css +++ b/src/main/resources/theme/icons.css @@ -345,4 +345,12 @@ .note-icon { -fx-shape: "M28.681 7.159c-0.694-0.947-1.662-2.053-2.724-3.116s-2.169-2.030-3.116-2.724c-1.612-1.182-2.393-1.319-2.841-1.319h-15.5c-1.378 0-2.5 1.121-2.5 2.5v27c0 1.378 1.122 2.5 2.5 2.5h23c1.378 0 2.5-1.122 2.5-2.5v-19.5c0-0.448-0.137-1.23-1.319-2.841zM24.543 5.457c0.959 0.959 1.712 1.825 2.268 2.543h-4.811v-4.811c0.718 0.556 1.584 1.309 2.543 2.268zM28 29.5c0 0.271-0.229 0.5-0.5 0.5h-23c-0.271 0-0.5-0.229-0.5-0.5v-27c0-0.271 0.229-0.5 0.5-0.5 0 0 15.499-0 15.5 0v7c0 0.552 0.448 1 1 1h7v19.5z M23 26h-14c-0.552 0-1-0.448-1-1s0.448-1 1-1h14c0.552 0 1 0.448 1 1s-0.448 1-1 1z M23 22h-14c-0.552 0-1-0.448-1-1s0.448-1 1-1h14c0.552 0 1 0.448 1 1s-0.448 1-1 1z M23 18h-14c-0.552 0-1-0.448-1-1s0.448-1 1-1h14c0.552 0 1 0.448 1 1s-0.448 1-1 1z"; +} + +.reply-icon { + -fx-shape: "M14 24.238v7.762l-12-12 12-12v7.932c13.961 0.327 13.362-9.493 9.808-15.932 8.772 9.482 6.909 24.674-9.808 24.238z"; +} + +.react-icon { + -fx-shape: "M30.688 13.313l-0.063 0.125h0.063v2.563c0 0.375-0.063 0.688-0.188 1l-4.063 9.375c-0.375 0.938-1.313 1.625-2.438 1.625h-12c-1.438 0-2.688-1.25-2.688-2.688v-13.313c0-0.75 0.313-1.375 0.813-1.875l8.75-8.813 1.438 1.438c0.375 0.375 0.563 0.813 0.563 1.375v0.438l-1.313 6.125h8.438c1.438 0 2.688 1.188 2.688 2.625zM1.313 28v-16h5.375v16h-5.375z"; } \ No newline at end of file diff --git a/src/main/resources/theme/style.css b/src/main/resources/theme/style.css index 27216bd64e..890dcd70b2 100644 --- a/src/main/resources/theme/style.css +++ b/src/main/resources/theme/style.css @@ -1404,6 +1404,10 @@ -fx-background-color: grey; } +.my-reaction { + -fx-background-color: -fx-accent; +} + .attribution-hyperlink { -fx-focus-color: transparent; -fx-border-color: transparent; diff --git a/src/test/java/com/faforever/client/builders/EmoticonBuilder.java b/src/test/java/com/faforever/client/builders/EmoticonBuilder.java index 04c6d56ba5..b422adcdb9 100644 --- a/src/test/java/com/faforever/client/builders/EmoticonBuilder.java +++ b/src/test/java/com/faforever/client/builders/EmoticonBuilder.java @@ -37,6 +37,6 @@ public EmoticonBuilder image(Image image) { } public Emoticon get() { - return new Emoticon(shortcodes, base64SvgContent, image); + return new Emoticon(shortcodes, base64SvgContent); } } diff --git a/src/test/java/com/faforever/client/chat/ChannelTabControllerTest.java b/src/test/java/com/faforever/client/chat/ChannelTabControllerTest.java index 2bd874b887..9f822f62e4 100644 --- a/src/test/java/com/faforever/client/chat/ChannelTabControllerTest.java +++ b/src/test/java/com/faforever/client/chat/ChannelTabControllerTest.java @@ -26,12 +26,6 @@ import org.mockito.Mock; import org.mockito.Spy; -import java.util.regex.Pattern; - -import static com.faforever.client.theme.ThemeService.CHAT_CONTAINER; -import static com.faforever.client.theme.ThemeService.CHAT_SECTION_COMPACT; -import static com.faforever.client.theme.ThemeService.CHAT_TEXT_COMPACT; -import static com.faforever.client.theme.ThemeService.CHAT_TEXT_EXTENDED; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -90,16 +84,7 @@ public void setUp() throws Exception { lenient().when(chatMessageViewController.chatChannelProperty()).thenReturn(new SimpleObjectProperty<>()); lenient().when(emoticonsWindowController.getRoot()).thenReturn(new VBox()); lenient().when(chatService.getCurrentUsername()).thenReturn(user.getUsername()); - lenient().when(themeService.getThemeFileUrl(CHAT_CONTAINER)) - .thenReturn(getClass().getResource("/theme/chat/chat_container.html")); - lenient().when(themeService.getThemeFileUrl(CHAT_SECTION_COMPACT)) - .thenReturn(getClass().getResource("/theme/chat/compact/chat_section.html")); - lenient().when(themeService.getThemeFileUrl(CHAT_TEXT_COMPACT)) - .thenReturn(getClass().getResource("/theme/chat/compact/chat_text.html")); - lenient().when(themeService.getThemeFileUrl(CHAT_TEXT_EXTENDED)).thenReturn( - getClass().getResource("/theme/chat/extended/chat_text.html")); lenient().when(timeService.asShortTime(any())).thenReturn("now"); - lenient().when(emoticonService.getEmoticonShortcodeDetectorPattern()).thenReturn(Pattern.compile("-----")); lenient().when(chatService.getOrCreateChatUser(any(String.class), eq(defaultChatChannel.getName()))) .thenReturn(new ChatChannelUser("junit", defaultChatChannel)); when(chatUserListController.chatChannelProperty()).thenReturn(new SimpleObjectProperty<>()); diff --git a/src/test/java/com/faforever/client/chat/ChatChannelTest.java b/src/test/java/com/faforever/client/chat/ChatChannelTest.java index 2fe4609d36..50f270cdb6 100644 --- a/src/test/java/com/faforever/client/chat/ChatChannelTest.java +++ b/src/test/java/com/faforever/client/chat/ChatChannelTest.java @@ -34,8 +34,8 @@ public void testPartyChannel() { public void testMessageMax() { ChatChannel channel = new ChatChannel("#test"); ChatChannelUser sender = ChatChannelUserBuilder.create("", channel).defaultValues().get(); - channel.addMessage(new ChatMessage("1", Instant.now().minusSeconds(1), sender, "1", Type.MESSAGE)); - channel.addMessage(new ChatMessage("2", Instant.now(), sender, "2", Type.MESSAGE)); + channel.addMessage(new ChatMessage("1", Instant.now().minusSeconds(1), sender, "1", Type.MESSAGE, null)); + channel.addMessage(new ChatMessage("2", Instant.now(), sender, "2", Type.MESSAGE, null)); assertThat(channel.getMessages(), hasSize(2)); @@ -44,7 +44,7 @@ public void testMessageMax() { assertThat(channel.getMessages(), hasSize(1)); assertEquals("2", channel.getMessages().getLast().getContent()); - channel.addMessage(new ChatMessage("3", Instant.now(), sender, "3", Type.MESSAGE)); + channel.addMessage(new ChatMessage("3", Instant.now(), sender, "3", Type.MESSAGE, null)); assertThat(channel.getMessages(), hasSize(1)); assertEquals("3", channel.getMessages().getLast().getContent()); diff --git a/src/test/java/com/faforever/client/chat/ChatMessageControllerTest.java b/src/test/java/com/faforever/client/chat/ChatMessageControllerTest.java index 8ba0889532..206bf407ec 100644 --- a/src/test/java/com/faforever/client/chat/ChatMessageControllerTest.java +++ b/src/test/java/com/faforever/client/chat/ChatMessageControllerTest.java @@ -3,7 +3,9 @@ import com.faforever.client.avatar.AvatarService; import com.faforever.client.builders.PlayerBeanBuilder; import com.faforever.client.chat.ChatMessage.Type; +import com.faforever.client.chat.emoticons.Emoticon; import com.faforever.client.chat.emoticons.EmoticonService; +import com.faforever.client.chat.emoticons.EmoticonsWindowController; import com.faforever.client.domain.PlayerBean; import com.faforever.client.fx.ImageViewHelper; import com.faforever.client.fx.MouseEvents; @@ -11,6 +13,7 @@ import com.faforever.client.i18n.I18n; import com.faforever.client.player.CountryFlagService; import com.faforever.client.test.PlatformTest; +import com.faforever.client.theme.UiService; import com.faforever.client.util.TimeService; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.ObservableList; @@ -20,27 +23,38 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.MouseButton; +import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; +import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.text.Text; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import java.time.Instant; +import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import java.util.regex.Pattern; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.mockito.ArgumentCaptor.captor; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class ChatMessageControllerTest extends PlatformTest { @@ -60,6 +74,8 @@ public class ChatMessageControllerTest extends PlatformTest { private ImageViewHelper imageViewHelper; @Mock private I18n i18n; + @Mock + private UiService uiService; @InjectMocks private ChatMessageController instance; @@ -77,8 +93,6 @@ public void setup() throws Exception { lenient().when(chatService.getCurrentUsername()).thenReturn("junit"); lenient().when(chatService.getMentionPattern()) .thenReturn(Pattern.compile("(^|[^A-Za-z0-9-])junit([^A-Za-z0-9-]|$)")); - lenient().when(emoticonService.getEmoticonShortcodeDetectorPattern()).thenReturn(Pattern.compile(":\\)")); - lenient().when(emoticonService.getImageByShortcode(any())).thenReturn(image); lenient().when(timeService.asShortTime(any())).thenReturn("now"); lenient().when(imageViewHelper.createPlaceholderImageOnErrorObservable(any())) .thenReturn(new SimpleObjectProperty<>(image)); @@ -92,23 +106,23 @@ public void setup() throws Exception { @Test public void testPending() { - instance.setChatMessage(new ChatMessage(null, Instant.now(), user, "", Type.PENDING)); + instance.setChatMessage(new ChatMessage(null, Instant.now(), user, "", Type.PENDING, null)); assertThat(instance.timeLabel.getText(), equalTo("pending")); - instance.setChatMessage(new ChatMessage(null, Instant.now(), user, "", Type.MESSAGE)); + instance.setChatMessage(new ChatMessage(null, Instant.now(), user, "", Type.MESSAGE, null)); assertThat(instance.timeLabel.getText(), not(equalTo("pending"))); } @Test public void testClickAuthor() { - instance.setChatMessage(new ChatMessage(null, Instant.now(), user, "", Type.MESSAGE)); + instance.setChatMessage(new ChatMessage(null, Instant.now(), user, "", Type.MESSAGE, null)); runOnFxThreadAndWait(() -> instance.authorLabel.fireEvent(MouseEvents.generateClick(MouseButton.PRIMARY, 2))); - verify(chatService).onInitiatePrivateChat("junit"); + verify(chatService).joinPrivateChat("junit"); } @Test public void testMultipleWords() { - instance.setChatMessage(new ChatMessage(null, Instant.now(), user, "Hello world!", Type.MESSAGE)); + instance.setChatMessage(new ChatMessage(null, Instant.now(), user, "Hello world!", Type.MESSAGE, null)); ObservableList children = instance.message.getChildren(); assertThat(children, hasSize(2)); @@ -128,7 +142,7 @@ public void testMultipleWords() { @Test public void testChannel() { - instance.setChatMessage(new ChatMessage(null, Instant.now(), user, "#test", Type.MESSAGE)); + instance.setChatMessage(new ChatMessage(null, Instant.now(), user, "#test", Type.MESSAGE, null)); ObservableList children = instance.message.getChildren(); assertThat(children, hasSize(1)); @@ -144,7 +158,7 @@ public void testChannel() { @Test public void testUrl() { - instance.setChatMessage(new ChatMessage(null, Instant.now(), user, "https://www.google.com", Type.MESSAGE)); + instance.setChatMessage(new ChatMessage(null, Instant.now(), user, "https://www.google.com", Type.MESSAGE, null)); ObservableList children = instance.message.getChildren(); assertThat(children, hasSize(1)); @@ -160,7 +174,7 @@ public void testUrl() { @Test public void testSelf() { - instance.setChatMessage(new ChatMessage(null, Instant.now(), user, "junit", Type.MESSAGE)); + instance.setChatMessage(new ChatMessage(null, Instant.now(), user, "junit", Type.MESSAGE, null)); ObservableList children = instance.message.getChildren(); assertThat(children, hasSize(1)); @@ -175,7 +189,10 @@ public void testSelf() { @Test public void testEmoticon() { - instance.setChatMessage(new ChatMessage(null, Instant.now(), user, ":)", Type.MESSAGE)); + when(emoticonService.isEmoticonShortcode(any())).thenReturn(true); + when(emoticonService.getImageByShortcode(any())).thenReturn(image); + + instance.setChatMessage(new ChatMessage(null, Instant.now(), user, ":)", Type.MESSAGE, null)); ObservableList children = instance.message.getChildren(); assertThat(children, hasSize(1)); @@ -195,4 +212,67 @@ public void testEmoticon() { verify(emoticonService).getImageByShortcode(":)"); } + + @Test + public void testReactionChange() { + ReactionController mockedReactionController = mock(ReactionController.class); + when(uiService.loadFxml("theme/chat/emoticons/reaction.fxml")).thenReturn(mockedReactionController); + when(mockedReactionController.getRoot()).thenReturn(new HBox()); + when(mockedReactionController.onReactionClickedProperty()).thenReturn(new SimpleObjectProperty<>()); + + Emoticon emoticon = new Emoticon(List.of(), ""); + ChatMessage message = new ChatMessage(null, Instant.now(), user, "hello", Type.MESSAGE, null); + instance.setChatMessage(message); + + assertThat(instance.reactionsContainer.isVisible(), is(false)); + assertThat(instance.reactionsContainer.getChildren(), empty()); + + runOnFxThreadAndWait(() -> message.addReaction(emoticon, new ChatChannelUser("junit", new ChatChannel("junit")))); + + assertThat(instance.reactionsContainer.isVisible(), is(true)); + assertThat(instance.reactionsContainer.getChildren(), hasSize(1)); + } + + @Test + public void testOnReact() { + EmoticonsWindowController mockedEmoticonsWindowController = mock(EmoticonsWindowController.class); + when(uiService.loadFxml("theme/chat/emoticons/emoticons_window.fxml")).thenReturn(mockedEmoticonsWindowController); + when(mockedEmoticonsWindowController.getRoot()).thenReturn(new VBox()); + + ChatMessage message = new ChatMessage(null, Instant.now(), user, "hello", Type.MESSAGE, null); + instance.setChatMessage(message); + instance.onReact(); + + ArgumentCaptor> captor = captor(); + + verify(mockedEmoticonsWindowController).setOnEmoticonClicked(captor.capture()); + + Consumer consumer = captor.getValue(); + Emoticon emoticon = new Emoticon(List.of(), ""); + runOnFxThreadAndWait(() -> consumer.accept(emoticon)); + + verify(chatService).reactToMessageInBackground(message, emoticon); + } + + @Test + public void testOnReactAlreadyReacted() { + EmoticonsWindowController mockedEmoticonsWindowController = mock(EmoticonsWindowController.class); + when(uiService.loadFxml("theme/chat/emoticons/emoticons_window.fxml")).thenReturn(mockedEmoticonsWindowController); + when(mockedEmoticonsWindowController.getRoot()).thenReturn(new VBox()); + + Emoticon emoticon = new Emoticon(List.of(), ""); + ChatMessage message = new ChatMessage(null, Instant.now(), user, "hello", Type.MESSAGE, null); + message.addReaction(emoticon, new ChatChannelUser("junit", new ChatChannel("junit"))); + instance.setChatMessage(message); + instance.onReact(); + + ArgumentCaptor> captor = captor(); + + verify(mockedEmoticonsWindowController).setOnEmoticonClicked(captor.capture()); + + Consumer consumer = captor.getValue(); + runOnFxThreadAndWait(() -> consumer.accept(emoticon)); + + verify(chatService, never()).reactToMessageInBackground(message, emoticon); + } } diff --git a/src/test/java/com/faforever/client/chat/ChatMessagesViewControllerTest.java b/src/test/java/com/faforever/client/chat/ChatMessagesViewControllerTest.java index a441755096..3d13450bc9 100644 --- a/src/test/java/com/faforever/client/chat/ChatMessagesViewControllerTest.java +++ b/src/test/java/com/faforever/client/chat/ChatMessagesViewControllerTest.java @@ -1,5 +1,6 @@ package com.faforever.client.chat; +import com.faforever.client.chat.emoticons.Emoticon; import com.faforever.client.chat.emoticons.EmoticonService; import com.faforever.client.chat.emoticons.EmoticonsWindowController; import com.faforever.client.fx.WebViewConfigurer; @@ -17,25 +18,25 @@ import javafx.stage.Stage; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; +import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; import java.util.regex.Pattern; -import static com.faforever.client.theme.ThemeService.CHAT_CONTAINER; -import static com.faforever.client.theme.ThemeService.CHAT_SECTION_COMPACT; -import static com.faforever.client.theme.ThemeService.CHAT_SECTION_EXTENDED; -import static com.faforever.client.theme.ThemeService.CHAT_TEXT_COMPACT; -import static com.faforever.client.theme.ThemeService.CHAT_TEXT_EXTENDED; import static java.util.concurrent.CompletableFuture.completedFuture; import static java.util.regex.Pattern.CASE_INSENSITIVE; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentCaptor.captor; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.lenient; @@ -77,17 +78,6 @@ public class ChatMessagesViewControllerTest extends PlatformTest { @BeforeEach public void setup() throws Exception { - lenient().when(themeService.getThemeFileUrl(CHAT_CONTAINER)) - .thenReturn(getClass().getResource("/theme/chat/chat_container.html")); - lenient().when(themeService.getThemeFileUrl(CHAT_SECTION_COMPACT)) - .thenReturn(getClass().getResource("/theme/chat/compact/chat_section.html")); - lenient().when(themeService.getThemeFileUrl(CHAT_TEXT_COMPACT)) - .thenReturn(getClass().getResource("/theme/chat/compact/chat_text.html")); - lenient().when(themeService.getThemeFileUrl(CHAT_SECTION_EXTENDED)) - .thenReturn(getClass().getResource("/theme/chat/extended/chat_section.html")); - lenient().when(themeService.getThemeFileUrl(CHAT_TEXT_EXTENDED)) - .thenReturn(getClass().getResource("/theme/chat/extended/chat_text.html")); - lenient().when(emoticonService.getEmoticonShortcodeDetectorPattern()).thenReturn(Pattern.compile("-----")); lenient().when(chatService.getCurrentUsername()).thenReturn("junit"); lenient().when(timeService.asShortTime(any())).thenReturn("now"); lenient().when(emoticonsWindowController.getRoot()).thenReturn(new VBox()); @@ -177,4 +167,17 @@ public void testOnSendMessageFailed() { assertThat(instance.messageTextField.getText(), is(message)); assertThat(instance.messageTextField.isDisable(), is(false)); } + + @Test + public void testOnEmoticonClicked() { + ArgumentCaptor> captor = captor(); + + verify(emoticonsWindowController).setOnEmoticonClicked(captor.capture()); + + runOnFxThreadAndWait(() -> captor.getValue().accept(new Emoticon(List.of(":)"), ""))); + + String expected = " " + ":)" + " "; + assertEquals(" :) ", instance.messageTextField.getText()); + assertEquals(expected.length(), instance.messageTextField.getCaretPosition()); + } } diff --git a/src/test/java/com/faforever/client/chat/ChatUserItemControllerTest.java b/src/test/java/com/faforever/client/chat/ChatUserItemControllerTest.java index 24055c61ce..aaad45d67e 100644 --- a/src/test/java/com/faforever/client/chat/ChatUserItemControllerTest.java +++ b/src/test/java/com/faforever/client/chat/ChatUserItemControllerTest.java @@ -111,7 +111,7 @@ public void testGetPlayer() { @Test public void testSingleClickDoesNotInitiatePrivateChat() { runOnFxThreadAndWait(() -> instance.onItemClicked(MouseEvents.generateClick(MouseButton.PRIMARY, 1))); - verify(chatService, never()).onInitiatePrivateChat(any()); + verify(chatService, never()).joinPrivateChat(any()); } @Test @@ -120,7 +120,7 @@ public void testDoubleClickInitiatesPrivateChat() { runOnFxThreadAndWait(() -> instance.onItemClicked(MouseEvents.generateClick(MouseButton.PRIMARY, 2))); ArgumentCaptor captor = ArgumentCaptor.forClass(String.class); - verify(chatService, times(1)).onInitiatePrivateChat(captor.capture()); + verify(chatService, times(1)).joinPrivateChat(captor.capture()); assertEquals(USER_NAME, captor.getValue()); } diff --git a/src/test/java/com/faforever/client/chat/MatchmakingChatControllerTest.java b/src/test/java/com/faforever/client/chat/MatchmakingChatControllerTest.java index 4305ee12e5..a3e6242ca7 100644 --- a/src/test/java/com/faforever/client/chat/MatchmakingChatControllerTest.java +++ b/src/test/java/com/faforever/client/chat/MatchmakingChatControllerTest.java @@ -22,9 +22,6 @@ import org.mockito.Mock; import org.mockito.Spy; -import static com.faforever.client.theme.ThemeService.CHAT_CONTAINER; -import static com.faforever.client.theme.ThemeService.CHAT_SECTION_COMPACT; -import static com.faforever.client.theme.ThemeService.CHAT_TEXT_COMPACT; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.lenient; @@ -74,12 +71,6 @@ public void setUp() throws Exception { lenient().when(chatService.getCurrentUsername()).thenReturn("junit"); lenient().when(i18n.get(anyString())).thenReturn(""); lenient().when(chatService.getOrCreateChannel("partyName")).thenReturn(new ChatChannel("partyName")); - lenient().when(themeService.getThemeFileUrl(CHAT_CONTAINER)).thenReturn( - getClass().getResource("/theme/chat/chat_container.html")); - lenient().when(themeService.getThemeFileUrl(CHAT_SECTION_COMPACT)).thenReturn( - getClass().getResource("/theme/chat/compact/chat_section.html")); - lenient().when(themeService.getThemeFileUrl(CHAT_TEXT_COMPACT)).thenReturn( - getClass().getResource("/theme/chat/compact/chat_text.html")); lenient().when(timeService.asShortTime(any())).thenReturn(""); loadFxml("theme/chat/matchmaking_chat.fxml", clazz -> { diff --git a/src/test/java/com/faforever/client/chat/emoticons/EmoticonControllerTest.java b/src/test/java/com/faforever/client/chat/emoticons/EmoticonControllerTest.java index f29658ec91..0201a0eb92 100644 --- a/src/test/java/com/faforever/client/chat/emoticons/EmoticonControllerTest.java +++ b/src/test/java/com/faforever/client/chat/emoticons/EmoticonControllerTest.java @@ -23,7 +23,7 @@ public class EmoticonControllerTest extends PlatformTest { @Mock private EmoticonService emoticonService; @Mock - private Consumer onAction; + private Consumer onAction; @InjectMocks private EmoticonController instance; @@ -41,7 +41,7 @@ public void testSetEmoticon() { .defaultValues() .image(new Image(new ClassPathResource("/images/hydro.png").getPath())) .get(); - runOnFxThreadAndWait(() -> instance.setEmoticon(emoticon, onAction)); + runOnFxThreadAndWait(() -> instance.setEmoticon(emoticon)); assertNotNull(instance.emoticonImageView.getImage()); } @@ -49,10 +49,12 @@ public void testSetEmoticon() { public void testEmoticonClicked() { Emoticon emoticon = EmoticonBuilder.create().defaultValues().get(); runOnFxThreadAndWait(() -> { - instance.setEmoticon(emoticon, onAction); + instance.setEmoticon(emoticon); + instance.setOnEmoticonClicked(onAction); instance.root.fireEvent(MouseEvents.generateClick(MouseButton.PRIMARY, 1)); }); - verify(onAction).accept(emoticon.shortcodes().getFirst()); + + verify(onAction).accept(emoticon); } @Test diff --git a/src/test/java/com/faforever/client/chat/emoticons/EmoticonServiceTest.java b/src/test/java/com/faforever/client/chat/emoticons/EmoticonServiceTest.java index d3ccf8091d..7e6dc05e93 100644 --- a/src/test/java/com/faforever/client/chat/emoticons/EmoticonServiceTest.java +++ b/src/test/java/com/faforever/client/chat/emoticons/EmoticonServiceTest.java @@ -37,16 +37,13 @@ public void testAllProductionShortcodesAreUnique() throws Exception { .thenReturn(new ObjectMapper().readValue(EmoticonService.EMOTICONS_JSON_FILE_RESOURCE.getInputStream(), EmoticonsGroup[].class)); assertDoesNotThrow(() -> instance.loadAndVerifyEmoticons()); - String shortcodePattern = instance.getEmoticonShortcodeDetectorPattern().pattern(); - assertFalse(shortcodePattern.isBlank()); assertFalse(instance.getEmoticonsGroups().isEmpty()); instance.getEmoticonsGroups() .stream() .flatMap(emoticonsGroup -> emoticonsGroup.emoticons().stream()) .flatMap(emoticon -> emoticon.shortcodes().stream()) .forEach(shortcode -> { - assertTrue(shortcodePattern.contains(shortcode)); - assertFalse(instance.getBase64SvgContentByShortcode(shortcode).isBlank()); + assertTrue(instance.isEmoticonShortcode(shortcode)); }); } @@ -95,18 +92,15 @@ public void testThrowWhenShortcodesAreNotUniqueAcrossGroups() throws Exception { } @Test - public void testGetSvgContentByShortcode() throws Exception { + public void testLoadEmoticons() throws Exception { EmoticonsGroup emoticonsGroup = EmoticonGroupBuilder.create().defaultValues().get(); Emoticon emoticon = emoticonsGroup.emoticons().getFirst(); EmoticonsGroup[] emoticonsGroupsArray = new EmoticonsGroup[]{emoticonsGroup}; when(objectMapper.readValue(any(InputStream.class), eq(EmoticonsGroup[].class))).thenReturn(emoticonsGroupsArray); instance.loadAndVerifyEmoticons(); - assertEquals(emoticon.base64SvgContent(), - instance.getBase64SvgContentByShortcode(emoticon.shortcodes().getFirst())); + emoticon.shortcodes() - .forEach( - shortcode -> assertTrue(instance.getEmoticonShortcodeDetectorPattern().pattern().contains(shortcode))); - assertTrue(instance.getEmoticonShortcodeDetectorPattern().pattern().contains("|")); + .forEach(shortcode -> assertTrue(instance.isEmoticonShortcode(shortcode))); } } diff --git a/src/test/java/com/faforever/client/chat/emoticons/EmoticonsGroupControllerTest.java b/src/test/java/com/faforever/client/chat/emoticons/EmoticonsGroupControllerTest.java index f5ddbab9e8..473917933b 100644 --- a/src/test/java/com/faforever/client/chat/emoticons/EmoticonsGroupControllerTest.java +++ b/src/test/java/com/faforever/client/chat/emoticons/EmoticonsGroupControllerTest.java @@ -4,6 +4,7 @@ import com.faforever.client.fx.PlatformService; import com.faforever.client.test.PlatformTest; import com.faforever.client.theme.UiService; +import javafx.beans.property.SimpleObjectProperty; import javafx.scene.layout.AnchorPane; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,7 +15,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.verify; @@ -35,6 +35,7 @@ public void setUp() throws Exception { lenient().when(uiService.loadFxml("theme/chat/emoticons/emoticon.fxml")).thenReturn(emoticonController); lenient().when(emoticonController.getRoot()) .thenReturn(new AnchorPane(), new AnchorPane()); // FlowPane does not allow to put the same views + lenient().when(emoticonController.onEmoticonClickedProperty()).thenReturn(new SimpleObjectProperty<>()); loadFxml("theme/chat/emoticons/emoticons_group.fxml", clazz -> instance); } @@ -42,7 +43,7 @@ public void setUp() throws Exception { @Test public void testSetGroup() { EmoticonsGroup emoticonsGroup = EmoticonGroupBuilder.create().defaultValues().attribution("").get(); - runOnFxThreadAndWait(() -> instance.setGroup(emoticonsGroup, any())); + runOnFxThreadAndWait(() -> instance.setEmoticonsGroup(emoticonsGroup)); assertEquals(emoticonsGroup.name(), instance.groupLabel.getText()); assertFalse(instance.attributionPane.isVisible()); @@ -52,10 +53,10 @@ public void testSetGroup() { @Test public void testSetGroupWithAttribution() { EmoticonsGroup emoticonsGroup = EmoticonGroupBuilder.create().defaultValues().get(); - runOnFxThreadAndWait(() -> instance.setGroup(emoticonsGroup, any())); + runOnFxThreadAndWait(() -> instance.setEmoticonsGroup(emoticonsGroup)); assertEquals(emoticonsGroup.name(), instance.groupLabel.getText()); - assertEquals(emoticonsGroup.attribution(), instance.attributionHyperlink.getText()); + assertEquals(emoticonsGroup.attributionUrl(), instance.attributionHyperlink.getText()); assertTrue(instance.attributionPane.isVisible()); assertEquals(2, instance.emoticonsPane.getChildren().size()); } @@ -64,10 +65,10 @@ public void testSetGroupWithAttribution() { public void testOnClickAttribution() { EmoticonsGroup emoticonsGroup = EmoticonGroupBuilder.create().defaultValues().get(); runOnFxThreadAndWait(() -> { - instance.setGroup(emoticonsGroup, any()); + instance.setEmoticonsGroup(emoticonsGroup); instance.attributionHyperlink.fire(); }); - verify(platformService).showDocument(emoticonsGroup.attribution()); + verify(platformService).showDocument(emoticonsGroup.attributionUrl()); } @Test diff --git a/src/test/java/com/faforever/client/chat/emoticons/EmoticonsWindowControllerTest.java b/src/test/java/com/faforever/client/chat/emoticons/EmoticonsWindowControllerTest.java index 23f95dcf16..7fcef800c5 100644 --- a/src/test/java/com/faforever/client/chat/emoticons/EmoticonsWindowControllerTest.java +++ b/src/test/java/com/faforever/client/chat/emoticons/EmoticonsWindowControllerTest.java @@ -3,8 +3,7 @@ import com.faforever.client.builders.EmoticonGroupBuilder; import com.faforever.client.test.PlatformTest; import com.faforever.client.theme.UiService; -import javafx.scene.control.TextField; -import javafx.scene.control.TextInputControl; +import javafx.beans.property.SimpleObjectProperty; import javafx.scene.layout.VBox; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -15,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; public class EmoticonsWindowControllerTest extends PlatformTest { @@ -26,24 +26,19 @@ public class EmoticonsWindowControllerTest extends PlatformTest { @Mock private EmoticonsGroupController emoticonsGroupController; - private TextInputControl textField; - private List emoticonsGroups; - @InjectMocks private EmoticonsWindowController instance; @BeforeEach public void setUp() throws Exception { - emoticonsGroups = List.of( - EmoticonGroupBuilder.create().defaultValues().get(), - EmoticonGroupBuilder.create().defaultValues().get() - ); + List emoticonsGroups = List.of(EmoticonGroupBuilder.create().defaultValues().get(), + EmoticonGroupBuilder.create().defaultValues().get()); when(emoticonService.getEmoticonsGroups()).thenReturn(emoticonsGroups); when(uiService.loadFxml("theme/chat/emoticons/emoticons_group.fxml")).thenReturn(emoticonsGroupController); when(emoticonsGroupController.getRoot()).thenReturn(new VBox(), new VBox()); // Root do not allow to put the same views - textField = new TextField(); - instance.setTextInputControl(textField); + lenient().when(emoticonsGroupController.onEmoticonClickedProperty()).thenReturn(new SimpleObjectProperty<>()); + loadFxml("theme/chat/emoticons/emoticons_window.fxml", clazz -> instance); } @@ -52,16 +47,6 @@ public void testSetEmoticonsGroupViews() { assertEquals(2, instance.root.getChildren().size()); } - @Test - public void testOnEmoticonClicked() { - String shortcode = emoticonsGroups.getFirst().emoticons().getFirst().shortcodes().getFirst(); - runOnFxThreadAndWait(() -> instance.onEmoticonClicked().accept(shortcode)); - - String expected = " " + shortcode + " "; - assertEquals(expected, textField.getText()); - assertEquals(expected.length(), textField.getCaretPosition()); - } - @Test public void testGetRoot() { assertNotNull(instance.getRoot()); diff --git a/src/test/java/com/faforever/client/chat/emoticons/ReactionControllerTest.java b/src/test/java/com/faforever/client/chat/emoticons/ReactionControllerTest.java new file mode 100644 index 0000000000..8de228708b --- /dev/null +++ b/src/test/java/com/faforever/client/chat/emoticons/ReactionControllerTest.java @@ -0,0 +1,68 @@ +package com.faforever.client.chat.emoticons; + +import com.faforever.client.builders.EmoticonBuilder; +import com.faforever.client.chat.ChatService; +import com.faforever.client.chat.ReactionController; +import com.faforever.client.fx.MouseEvents; +import com.faforever.client.test.PlatformTest; +import javafx.scene.image.Image; +import javafx.scene.input.MouseButton; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.core.io.ClassPathResource; + +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; + +public class ReactionControllerTest extends PlatformTest { + + @Mock + private EmoticonService emoticonService; + @Mock + private ChatService chatService; + @Mock + private Consumer onAction; + + @InjectMocks + private ReactionController instance; + + @BeforeEach + public void setUp() throws Exception { + lenient().when(emoticonService.getImageByShortcode(any())).thenReturn(new Image("http://localhost")); + + loadFxml("theme/chat/emoticons/reaction.fxml", clazz -> instance); + } + + @Test + public void testSetEmoticon() { + Emoticon emoticon = EmoticonBuilder.create() + .defaultValues() + .image(new Image(new ClassPathResource("/images/hydro.png").getPath())) + .get(); + runOnFxThreadAndWait(() -> instance.setReaction(emoticon)); + assertNotNull(instance.emoticonImageView.getImage()); + } + + @Test + public void testEmoticonClicked() { + Emoticon emoticon = EmoticonBuilder.create().defaultValues().get(); + runOnFxThreadAndWait(() -> { + instance.setReaction(emoticon); + instance.setOnReactionClicked(onAction); + instance.root.fireEvent(MouseEvents.generateClick(MouseButton.PRIMARY, 1)); + }); + + verify(onAction).accept(emoticon); + } + + @Test + public void testGetRoot() { + assertNotNull(instance.getRoot()); + } +} diff --git a/src/test/java/com/faforever/client/fx/contextmenu/SendPrivateMessageClanLeaderMenuItemTest.java b/src/test/java/com/faforever/client/fx/contextmenu/SendPrivateMessageClanLeaderMenuItemTest.java index e963549eaa..ed75acc7e8 100644 --- a/src/test/java/com/faforever/client/fx/contextmenu/SendPrivateMessageClanLeaderMenuItemTest.java +++ b/src/test/java/com/faforever/client/fx/contextmenu/SendPrivateMessageClanLeaderMenuItemTest.java @@ -44,7 +44,7 @@ public void testSendMessageClanLeader() { instance.setObject(PlayerBeanBuilder.create().get()); instance.onClicked(); - verify(chatService).onInitiatePrivateChat(any()); + verify(chatService).joinPrivateChat(any()); } @Test diff --git a/src/test/java/com/faforever/client/fx/contextmenu/SendPrivateMessageMenuItemTest.java b/src/test/java/com/faforever/client/fx/contextmenu/SendPrivateMessageMenuItemTest.java index 0ae83969ca..514aa25e99 100644 --- a/src/test/java/com/faforever/client/fx/contextmenu/SendPrivateMessageMenuItemTest.java +++ b/src/test/java/com/faforever/client/fx/contextmenu/SendPrivateMessageMenuItemTest.java @@ -42,7 +42,7 @@ public void testSendPrivateMessage() { instance.setObject(username); instance.onClicked(); - verify(chatService).onInitiatePrivateChat(any()); + verify(chatService).joinPrivateChat(any()); } @Test