Skip to content

Commit

Permalink
Add reaction capability to chat messages
Browse files Browse the repository at this point in the history
  • Loading branch information
Sheikah45 committed Jan 8, 2024
1 parent d927569 commit 8d65c11
Show file tree
Hide file tree
Showing 51 changed files with 841 additions and 451 deletions.
4 changes: 4 additions & 0 deletions src/main/java/com/faforever/client/chat/ChatChannel.java
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@ public Optional<ChatChannelUser> getUser(String username) {
return Optional.ofNullable(usernameToChatUser.get(username));
}

public Optional<ChatMessage> getMessage(String id) {
return Optional.ofNullable(messagesById.get(id));

Check warning on line 149 in src/main/java/com/faforever/client/chat/ChatChannel.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/com/faforever/client/chat/ChatChannel.java#L149

Added line #L149 was not covered by tests
}

public void removePendingMessage(String messageId) {
messagesById.computeIfPresent(messageId,
(ignored, chatMessage) -> chatMessage.getType() == Type.PENDING ? null : chatMessage);
Expand Down
27 changes: 26 additions & 1 deletion src/main/java/com/faforever/client/chat/ChatMessage.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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

Check warning on line 28 in src/main/java/com/faforever/client/chat/ChatMessage.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/com/faforever/client/chat/ChatMessage.java#L28

Added line #L28 was not covered by tests
private final ChatMessage targetMessage;

private final ObservableMap<Emoticon, ObservableSet<String>> reactions = FXCollections.synchronizedObservableMap(
FXCollections.observableHashMap());
private final ObservableMap<Emoticon, ObservableSet<String>> unmodifiableReactions = FXCollections.unmodifiableObservableMap(
reactions);

public ObservableMap<Emoticon, ObservableSet<String>> 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
Expand Down
110 changes: 103 additions & 7 deletions src/main/java/com/faforever/client/chat/ChatMessageController.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,48 @@

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;
import javafx.beans.property.SimpleObjectProperty;
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;
Expand All @@ -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
Expand All @@ -54,8 +70,10 @@ public class ChatMessageController extends NodeController<VBox> {
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;
Expand All @@ -65,11 +83,18 @@ public class ChatMessageController extends NodeController<VBox> {
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> chatMessage = new SimpleObjectProperty<>();
private final BooleanProperty showDetails = new SimpleBooleanProperty();
private final StringProperty inlineTextColorStyleProperty = new SimpleStringProperty();
private final MapChangeListener<Emoticon, ObservableSet<String>> reactionChangeListener = this::onReactionChange;

private final Map<Emoticon, HBox> reactionNodeMap = new HashMap<>();

private Pattern mentionPattern;

Expand Down Expand Up @@ -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()
Expand All @@ -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);

Check warning on line 156 in src/main/java/com/faforever/client/chat/ChatMessageController.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/com/faforever/client/chat/ChatMessageController.java#L156

Added line #L156 was not covered by tests
}

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<? extends Emoticon, ? extends ObservableSet<String>> change) {
Emoticon reaction = change.getKey();
if (change.wasRemoved()) {
HBox reactionRoot = reactionNodeMap.remove(reaction);
fxApplicationThreadExecutor.execute(() -> reactionsContainer.getChildren().remove(reactionRoot));

Check warning on line 176 in src/main/java/com/faforever/client/chat/ChatMessageController.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/com/faforever/client/chat/ChatMessageController.java#L175-L176

Added lines #L175 - L176 were not covered by tests
}

if (change.wasAdded()) {
addReaction(reaction, change.getValueAdded());
}
}

private void addReaction(Emoticon reaction, ObservableSet<String> 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);
}

Check warning on line 198 in src/main/java/com/faforever/client/chat/ChatMessageController.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/com/faforever/client/chat/ChatMessageController.java#L197-L198

Added lines #L197 - L198 were not covered by tests

public void onMouseEntered() {
messageActionsContainer.setVisible(true);

Check warning on line 201 in src/main/java/com/faforever/client/chat/ChatMessageController.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/com/faforever/client/chat/ChatMessageController.java#L201

Added line #L201 was not covered by tests
}

private List<? extends Node> 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 + " ");
};
}
Expand Down Expand Up @@ -203,4 +273,30 @@ public BooleanProperty showDetailsProperty() {
public void setShowDetails(boolean showDetails) {
this.showDetails.set(showDetails);
}

public void onReply() {}

Check warning on line 277 in src/main/java/com/faforever/client/chat/ChatMessageController.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/com/faforever/client/chat/ChatMessageController.java#L277

Added line #L277 was not covered by tests

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);
}

Check warning on line 301 in src/main/java/com/faforever/client/chat/ChatMessageController.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/com/faforever/client/chat/ChatMessageController.java#L300-L301

Added lines #L300 - L301 were not covered by tests
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ChatMessageViewController extends NodeController<VBox> {

private static final String ACTION_PREFIX = "/me ";

private final ObjectFactory<ChatMessageItemCell> chatMessageItemCellFactory;
private final NotificationService notificationService;
private final ChatService chatService;
Expand All @@ -69,8 +67,8 @@ public class ChatMessageViewController extends NodeController<VBox> {
public TextField messageTextField;
public ListView<ChatMessage> messagesListView;
public VBox root;
public Node emoticonsWindow;
public Label typingLabel;
public Node emoticonsWindow;
public EmoticonsWindowController emoticonsWindowController;

private final List<String> userMessageHistory = new ArrayList<>();
Expand All @@ -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);
Expand Down Expand Up @@ -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())));

Check warning on line 137 in src/main/java/com/faforever/client/chat/ChatMessageViewController.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/com/faforever/client/chat/ChatMessageViewController.java#L137

Added line #L137 was not covered by tests
}

private AutoCompletionHelper createAutoCompletionHelper() {
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/com/faforever/client/chat/ChatService.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,6 +21,10 @@ public interface ChatService {

void disconnect();

CompletableFuture<Void> reactToMessageInBackground(ChatMessage targetMessage, Emoticon reaction);

CompletableFuture<Void> sendReplyInBackground(ChatMessage targetMessage, String message);

CompletableFuture<Void> sendMessageInBackground(ChatChannel chatChannel, String message);

boolean userExistsInAnyChannel(String username);
Expand Down Expand Up @@ -52,7 +57,7 @@ default void leaveChannel(String channelName) {

void setChannelTopic(ChatChannel chatChannel, String text);

void onInitiatePrivateChat(String username);
void joinPrivateChat(String username);

Set<ChatChannel> getChannels();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}

Expand Down

0 comments on commit 8d65c11

Please sign in to comment.