Contact Deletion #97
@ -21,11 +21,12 @@ import envoy.event.MessageStatusChange;
|
||||
*/
|
||||
public class Chat implements Serializable {
|
||||
|
||||
protected final Contact recipient;
|
||||
|
||||
protected transient ObservableList<Message> messages = FXCollections.observableArrayList();
|
||||
|
||||
protected int unreadAmount;
|
||||
protected boolean disabled;
|
||||
|
||||
protected final Contact recipient;
|
||||
|
||||
kske marked this conversation as resolved
|
||||
/**
|
||||
* Stores the last time an {@link envoy.event.IsTyping} event has been sent.
|
||||
@ -55,7 +56,15 @@ public class Chat implements Serializable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() { return String.format("%s[recipient=%s,messages=%d]", getClass().getSimpleName(), recipient, messages.size()); }
|
||||
public String toString() {
|
||||
return String.format(
|
||||
"%s[recipient=%s,messages=%d,disabled=%b]",
|
||||
getClass().getSimpleName(),
|
||||
recipient,
|
||||
messages.size(),
|
||||
disabled
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a hash code based on the recipient.
|
||||
@ -90,7 +99,9 @@ public class Chat implements Serializable {
|
||||
public void read(WriteProxy writeProxy) {
|
||||
for (int i = messages.size() - 1; i >= 0; --i) {
|
||||
final var m = messages.get(i);
|
||||
if (m.getSenderID() == recipient.getID()) if (m.getStatus() == MessageStatus.READ) break;
|
||||
if (m.getSenderID() == recipient.getID())
|
||||
if (m.getStatus() == MessageStatus.READ)
|
||||
break;
|
||||
else {
|
||||
m.setStatus(MessageStatus.READ);
|
||||
writeProxy.writeMessageStatusChange(new MessageStatusChange(m));
|
||||
@ -168,4 +179,22 @@ public class Chat implements Serializable {
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
public void lastWritingEventWasNow() { lastWritingEvent = System.currentTimeMillis(); }
|
||||
|
||||
/**
|
||||
* Determines whether messages can be sent in this chat. Should be {@code true}
|
||||
* i.e. for chats whose recipient deleted this client as a contact.
|
||||
*
|
||||
* @return whether this chat has been disabled
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
delvh marked this conversation as resolved
kske
commented
Doesn't sound very conclusive to me. Doesn't sound very conclusive to me.
|
||||
public boolean isDisabled() { return disabled; }
|
||||
|
||||
/**
|
||||
* Determines whether messages can be sent in this chat. Should be true i.e. for
|
||||
* chats whose recipient deleted this client as a contact.
|
||||
*
|
||||
* @param disabled whether this chat should be disabled
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public void setDisabled(boolean disabled) { this.disabled = disabled; }
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ public final class GroupChat extends Chat {
|
||||
* @param recipient the group whose members receive the messages
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
public GroupChat(User sender, Contact recipient) {
|
||||
public GroupChat(User sender, Group recipient) {
|
||||
super(recipient);
|
||||
this.sender = sender;
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
package envoy.client.data;
|
||||
|
||||
import static java.util.function.Predicate.not;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.channels.*;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.logging.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.*;
|
||||
@ -14,6 +17,7 @@ import envoy.client.event.*;
|
||||
import envoy.data.*;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
import envoy.event.*;
|
||||
import envoy.event.contact.*;
|
||||
import envoy.exception.EnvoyException;
|
||||
import envoy.util.*;
|
||||
|
||||
@ -39,6 +43,7 @@ public final class LocalDB implements EventListener {
|
||||
private IDGenerator idGenerator;
|
||||
private CacheMap cacheMap = new CacheMap();
|
||||
private String authToken;
|
||||
private boolean contactsChanged;
|
||||
|
||||
// Auto save timer
|
||||
private Timer autoSaver;
|
||||
@ -137,6 +142,31 @@ public final class LocalDB implements EventListener {
|
||||
userFile = new File(dbDir, user.getID() + ".db");
|
||||
try (var in = new ObjectInputStream(new FileInputStream(userFile))) {
|
||||
chats = FXCollections.observableList((List<Chat>) in.readObject());
|
||||
|
||||
// Some chats have changed and should not be overwritten by the saved values
|
||||
if (contactsChanged) {
|
||||
final var contacts = user.getContacts();
|
||||
|
||||
// Mark chats as disabled if a contact is no longer in this users contact list
|
||||
final var changedUserChats = chats.stream()
|
||||
.filter(not(chat -> contacts.contains(chat.getRecipient())))
|
||||
.peek(chat -> { chat.setDisabled(true); logger.log(Level.INFO, String.format("Deleted chat with %s.", chat.getRecipient())); });
|
||||
|
||||
// Also update groups with a different member count
|
||||
final var changedGroupChats = contacts.stream().filter(Group.class::isInstance).flatMap(group -> {
|
||||
final var potentialChat = getChat(group.getID());
|
||||
if (potentialChat.isEmpty()) return Stream.empty();
|
||||
final var chat = potentialChat.get();
|
||||
if (group.getContacts().size() != chat.getRecipient().getContacts().size()) {
|
||||
logger.log(Level.INFO, "Removed one (or more) members from " + group);
|
||||
return Stream.of(chat);
|
||||
} else return Stream.empty();
|
||||
});
|
||||
Stream.concat(changedUserChats, changedGroupChats).forEach(chat -> chats.set(chats.indexOf(chat), chat));
|
||||
|
||||
// loadUserData can get called two (or more?) times during application lifecycle
|
||||
contactsChanged = false;
|
||||
}
|
||||
cacheMap = (CacheMap) in.readObject();
|
||||
lastSync = (Instant) in.readObject();
|
||||
} finally {
|
||||
@ -163,7 +193,7 @@ public final class LocalDB implements EventListener {
|
||||
user.getContacts()
|
||||
.stream()
|
||||
.filter(c -> !c.equals(user) && getChat(c.getID()).isEmpty())
|
||||
.map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, c))
|
||||
.map(c -> c instanceof User ? new Chat(c) : new GroupChat(user, (Group) c))
|
||||
.forEach(chats::add);
|
||||
}
|
||||
|
||||
@ -195,9 +225,9 @@ public final class LocalDB implements EventListener {
|
||||
* @throws IOException if the saving process failed
|
||||
* @since Envoy Client v0.3-alpha
|
||||
*/
|
||||
@Event(eventType = EnvoyCloseEvent.class, priority = 1000)
|
||||
@Event(eventType = EnvoyCloseEvent.class, priority = 500)
|
||||
private synchronized void save() {
|
||||
EnvoyLog.getLogger(LocalDB.class).log(Level.INFO, "Saving local database...");
|
||||
EnvoyLog.getLogger(LocalDB.class).log(Level.FINER, "Saving local database...");
|
||||
|
||||
// Save users
|
||||
try {
|
||||
@ -217,33 +247,57 @@ public final class LocalDB implements EventListener {
|
||||
}
|
||||
}
|
||||
|
||||
@Event(priority = 150)
|
||||
@Event(priority = 500)
|
||||
private void onMessage(Message msg) { if (msg.getStatus() == MessageStatus.SENT) msg.nextStatus(); }
|
||||
|
||||
@Event(priority = 150)
|
||||
@Event(priority = 500)
|
||||
private void onGroupMessage(GroupMessage msg) {
|
||||
// TODO: Cancel event once EventBus is updated
|
||||
if (msg.getStatus() == MessageStatus.WAITING || msg.getStatus() == MessageStatus.READ)
|
||||
logger.warning("The groupMessage has the unexpected status " + msg.getStatus());
|
||||
}
|
||||
|
||||
@Event(priority = 150)
|
||||
@Event(priority = 500)
|
||||
private void onMessageStatusChange(MessageStatusChange evt) { getMessage(evt.getID()).ifPresent(msg -> msg.setStatus(evt.get())); }
|
||||
|
||||
@Event(priority = 150)
|
||||
@Event(priority = 500)
|
||||
private void onGroupMessageStatusChange(GroupMessageStatusChange evt) {
|
||||
this.<GroupMessage>getMessage(evt.getID()).ifPresent(msg -> msg.getMemberStatuses().replace(evt.getMemberID(), evt.get()));
|
||||
}
|
||||
|
||||
@Event(priority = 150)
|
||||
@Event(priority = 500)
|
||||
private void onUserStatusChange(UserStatusChange evt) {
|
||||
getChat(evt.getID()).map(Chat::getRecipient).map(User.class::cast).ifPresent(u -> u.setStatus(evt.get()));
|
||||
}
|
||||
|
||||
@Event(priority = 150)
|
||||
@Event(priority = 500)
|
||||
private void onUserOperation(UserOperation operation) {
|
||||
final var eventUser = operation.get();
|
||||
switch (operation.getOperationType()) {
|
||||
case ADD:
|
||||
Platform.runLater(() -> chats.add(0, new Chat(eventUser)));
|
||||
break;
|
||||
case REMOVE:
|
||||
getChat(eventUser.getID()).ifPresent(chat -> chat.setDisabled(true));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Event
|
||||
private void onGroupCreationResult(GroupCreationResult evt) {
|
||||
final var newGroup = evt.get();
|
||||
|
||||
// The group creation was not successful
|
||||
if (newGroup == null) return;
|
||||
|
||||
// The group was successfully created
|
||||
else Platform.runLater(() -> chats.add(new GroupChat(user, newGroup)));
|
||||
}
|
||||
|
||||
@Event(priority = 500)
|
||||
private void onGroupResize(GroupResize evt) { getChat(evt.getGroupID()).map(Chat::getRecipient).map(Group.class::cast).ifPresent(evt::apply); }
|
||||
|
||||
@Event(priority = 150)
|
||||
@Event(priority = 500)
|
||||
private void onNameChange(NameChange evt) {
|
||||
chats.stream().map(Chat::getRecipient).filter(c -> c.getID() == evt.getID()).findAny().ifPresent(c -> c.setName(evt.get()));
|
||||
}
|
||||
@ -262,7 +316,7 @@ public final class LocalDB implements EventListener {
|
||||
*
|
||||
* @since Envoy Client v0.2-beta
|
||||
*/
|
||||
@Event(eventType = Logout.class, priority = 100)
|
||||
@Event(eventType = Logout.class, priority = 50)
|
||||
private void onLogout() {
|
||||
autoSaver.cancel();
|
||||
autoSaveRestart = true;
|
||||
@ -296,6 +350,12 @@ public final class LocalDB implements EventListener {
|
||||
@Event(priority = 500)
|
||||
private void onOwnStatusChange(OwnStatusChange statusChange) { user.setStatus(statusChange.get()); }
|
||||
|
||||
@Event(eventType = ContactsChangedSinceLastLogin.class, priority = 500)
|
||||
private void onContactsChangedSinceLastLogin() { contactsChanged = true; }
|
||||
|
||||
@Event(priority = 500)
|
||||
private void onContactDisabled(ContactDisabled event) { getChat(event.get().getID()).ifPresent(chat -> chat.setDisabled(true)); }
|
||||
|
||||
/**
|
||||
* @return a {@code Map<String, User>} of all users stored locally with their
|
||||
* user names as keys
|
||||
|
@ -68,7 +68,7 @@ public final class Settings implements EventListener {
|
||||
* @throws IOException if an error occurs while saving the themes
|
||||
* @since Envoy Client v0.2-alpha
|
||||
*/
|
||||
@Event(eventType = EnvoyCloseEvent.class, priority = 900)
|
||||
@Event(eventType = EnvoyCloseEvent.class)
|
||||
private void save() {
|
||||
EnvoyLog.getLogger(Settings.class).log(Level.INFO, "Saving settings...");
|
||||
|
||||
|
@ -6,6 +6,7 @@ import envoy.client.data.Context;
|
||||
import envoy.client.helper.ShutdownHelper;
|
||||
import envoy.client.ui.SceneContext.SceneInfo;
|
||||
import envoy.client.util.UserUtil;
|
||||
import envoy.data.User.UserStatus;
|
||||
|
||||
/**
|
||||
* Envoy-specific implementation of the keyboard-shortcut interaction offered by
|
||||
@ -40,5 +41,25 @@ public class EnvoyShortcutConfig {
|
||||
() -> Context.getInstance().getSceneContext().load(SceneInfo.SETTINGS_SCENE),
|
||||
SceneInfo.SETTINGS_SCENE,
|
||||
SceneInfo.LOGIN_SCENE);
|
||||
|
||||
// Add option to change to status away
|
||||
instance.addForNotExcluded(new KeyCodeCombination(KeyCode.A, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN),
|
||||
() -> UserUtil.changeStatus(UserStatus.AWAY),
|
||||
SceneInfo.LOGIN_SCENE);
|
||||
|
||||
// Add option to change to status busy
|
||||
instance.addForNotExcluded(new KeyCodeCombination(KeyCode.B, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN),
|
||||
() -> UserUtil.changeStatus(UserStatus.BUSY),
|
||||
SceneInfo.LOGIN_SCENE);
|
||||
|
||||
// Add option to change to status offline
|
||||
instance.addForNotExcluded(new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN),
|
||||
() -> UserUtil.changeStatus(UserStatus.OFFLINE),
|
||||
SceneInfo.LOGIN_SCENE);
|
||||
|
||||
// Add option to change to status online
|
||||
instance.addForNotExcluded(new KeyCodeCombination(KeyCode.N, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN),
|
||||
() -> UserUtil.changeStatus(UserStatus.ONLINE),
|
||||
SceneInfo.LOGIN_SCENE);
|
||||
}
|
||||
}
|
||||
|
21
client/src/main/java/envoy/client/event/ContactDisabled.java
Normal file
21
client/src/main/java/envoy/client/event/ContactDisabled.java
Normal file
@ -0,0 +1,21 @@
|
||||
package envoy.client.event;
|
||||
|
||||
import envoy.data.Contact;
|
||||
import envoy.event.Event;
|
||||
|
||||
/**
|
||||
* Signifies that the chat of a contact should be disabled.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public class ContactDisabled extends Event<Contact> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* @param contact the contact that should be disabled
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public ContactDisabled(Contact contact) { super(contact); }
|
||||
}
|
@ -61,6 +61,7 @@ public final class Client implements EventListener, Closeable {
|
||||
*/
|
||||
public void performHandshake(LoginCredentials credentials, CacheMap cacheMap) throws TimeoutException, IOException, InterruptedException {
|
||||
if (online) throw new IllegalStateException("Handshake has already been performed successfully");
|
||||
rejected = false;
|
||||
|
||||
// Establish TCP connection
|
||||
logger.log(Level.FINER, String.format("Attempting connection to server %s:%d...", config.getServer(), config.getPort()));
|
||||
@ -75,8 +76,6 @@ public final class Client implements EventListener, Closeable {
|
||||
receiver.registerProcessor(User.class, sender -> this.sender = sender);
|
||||
receiver.registerProcessors(cacheMap.getMap());
|
||||
|
||||
rejected = false;
|
||||
|
||||
// Start receiver
|
||||
receiver.start();
|
||||
|
||||
@ -95,7 +94,10 @@ public final class Client implements EventListener, Closeable {
|
||||
return;
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() - start > 5000) throw new TimeoutException("Did not log in after 5 seconds");
|
||||
if (System.currentTimeMillis() - start > 5000) {
|
||||
rejected = true;
|
||||
throw new TimeoutException("Did not log in after 5 seconds");
|
||||
}
|
||||
Thread.sleep(500);
|
||||
}
|
||||
|
||||
@ -146,7 +148,7 @@ public final class Client implements EventListener, Closeable {
|
||||
logger.log(Level.FINE, "Sending " + obj);
|
||||
try {
|
||||
SerializationUtils.writeBytesWithLength(obj, socket.getOutputStream());
|
||||
} catch (IOException e) {
|
||||
} catch (final IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
@ -177,7 +179,7 @@ public final class Client implements EventListener, Closeable {
|
||||
private void onHandshakeRejection() { rejected = true; }
|
||||
|
||||
@Override
|
||||
@Event(eventType = EnvoyCloseEvent.class, priority = 800)
|
||||
@Event(eventType = EnvoyCloseEvent.class, priority = 50)
|
||||
public void close() {
|
||||
if (online) {
|
||||
logger.log(Level.INFO, "Closing connection...");
|
||||
|
@ -16,5 +16,7 @@ public final class GroupSizeLabel extends Label {
|
||||
* @param recipient the group whose members to show
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public GroupSizeLabel(Group recipient) { super(recipient.getContacts().size() + " members"); }
|
||||
public GroupSizeLabel(Group recipient) {
|
||||
super(recipient.getContacts().size() + " member" + (recipient.getContacts().size() != 1 ? "s" : ""));
|
||||
delvh marked this conversation as resolved
kske
commented
We could just use parentheses, but I guess this is less ambiguous. We could just use parentheses, but I guess this is less ambiguous.
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ import envoy.data.*;
|
||||
import envoy.data.Attachment.AttachmentType;
|
||||
import envoy.data.Message.MessageStatus;
|
||||
import envoy.event.*;
|
||||
import envoy.event.contact.ContactOperation;
|
||||
import envoy.event.contact.UserOperation;
|
||||
import envoy.exception.EnvoyException;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
@ -91,9 +91,6 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
@FXML
|
||||
private Label topBarStatusLabel;
|
||||
|
||||
@FXML
|
||||
private MenuItem deleteContactMenuItem;
|
||||
|
||||
@FXML
|
||||
private ImageView attachmentView;
|
||||
|
||||
@ -165,7 +162,7 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
|
||||
// Initialize message and user rendering
|
||||
messageList.setCellFactory(MessageListCell::new);
|
||||
chatList.setCellFactory(new ListCellFactory<>(ChatControl::new));
|
||||
chatList.setCellFactory(ChatListCell::new);
|
||||
|
||||
// JavaFX provides an internal way of populating the context menu of a text
|
||||
// area.
|
||||
@ -191,7 +188,6 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
|
||||
// Set the design of the box in the upper-left corner
|
||||
settingsButton.setAlignment(Pos.BOTTOM_RIGHT);
|
||||
HBox.setHgrow(spaceBetweenUserAndSettingsButton, Priority.ALWAYS);
|
||||
generateOwnStatusControl();
|
||||
|
||||
Platform.runLater(() -> {
|
||||
@ -271,18 +267,22 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
}
|
||||
|
||||
@Event
|
||||
private void onContactOperation(ContactOperation operation) {
|
||||
final var contact = operation.get();
|
||||
switch (operation.getOperationType()) {
|
||||
case ADD:
|
||||
if (contact instanceof User) localDB.getUsers().put(contact.getName(), (User) contact);
|
||||
final var chat = contact instanceof User ? new Chat(contact) : new GroupChat(client.getSender(), contact);
|
||||
Platform.runLater(() -> ((ObservableList<Chat>) chats.getSource()).add(0, chat));
|
||||
break;
|
||||
case REMOVE:
|
||||
Platform.runLater(() -> chats.getSource().removeIf(c -> c.getRecipient().equals(contact)));
|
||||
break;
|
||||
private void onUserOperation(UserOperation operation) {
|
||||
|
||||
// All ADD dependent logic resides in LocalDB
|
||||
if (operation.getOperationType().equals(ElementOperation.REMOVE)) Platform.runLater(() -> disableChat(new ContactDisabled(operation.get())));
|
||||
}
|
||||
|
||||
@Event
|
||||
private void onGroupResize(GroupResize resize) {
|
||||
delvh marked this conversation as resolved
kske
commented
Why not use Why not use `Optional#ifPresent` here?
|
||||
final var chatFound = localDB.getChat(resize.getGroupID());
|
||||
chatFound.ifPresent(chat -> Platform.runLater(() -> {
|
||||
chatList.refresh();
|
||||
|
||||
// Update the top-bar status label if all conditions apply
|
||||
if (currentChat != null && currentChat.getRecipient().equals(chat.getRecipient())) topBarStatusLabel
|
||||
.setText(chat.getRecipient().getContacts().size() + " member" + (currentChat.getRecipient().getContacts().size() != 1 ? "s" : ""));
|
||||
}));
|
||||
}
|
||||
|
||||
@Event(eventType = NoAttachments.class)
|
||||
@ -298,8 +298,8 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
});
|
||||
}
|
||||
|
||||
@Event
|
||||
private void onGroupCreationResult(GroupCreationResult result) { Platform.runLater(() -> newGroupButton.setDisable(!result.get())); }
|
||||
@Event(priority = 150)
|
||||
private void onGroupCreationResult(GroupCreationResult result) { Platform.runLater(() -> newGroupButton.setDisable(result.get() == null)); }
|
||||
|
||||
@Event(eventType = ThemeChangeEvent.class)
|
||||
private void onThemeChange() {
|
||||
@ -312,7 +312,6 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
clientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
|
||||
chatList.setCellFactory(new ListCellFactory<>(ChatControl::new));
|
||||
messageList.setCellFactory(MessageListCell::new);
|
||||
// TODO: cache image
|
||||
if (currentChat != null)
|
||||
if (currentChat.getRecipient() instanceof User) recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
|
||||
else recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
|
||||
@ -332,8 +331,10 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
@FXML
|
||||
private void chatListClicked() {
|
||||
if (chatList.getSelectionModel().isEmpty()) return;
|
||||
final var chat = chatList.getSelectionModel().getSelectedItem();
|
||||
delvh marked this conversation as resolved
kske
commented
`currentChat != null` is unnecessary here.
delvh
commented
Let's hope I' ll stay unable to reproduce what I once have produced... Let's hope I' ll stay unable to reproduce what I once have produced...
|
||||
if (chat == null) return;
|
||||
|
||||
final var user = chatList.getSelectionModel().getSelectedItem().getRecipient();
|
||||
final var user = chat.getRecipient();
|
||||
if (user != null && (currentChat == null || !user.equals(currentChat.getRecipient()))) {
|
||||
|
||||
// LEON: JFC <===> JAVA FRIED CHICKEN <=/=> Java Foundation Classes
|
||||
@ -345,7 +346,6 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
final var scrollIndex = messageList.getItems().size() - currentChat.getUnreadAmount();
|
||||
messageList.scrollTo(scrollIndex);
|
||||
logger.log(Level.FINEST, "Loading chat with " + user + " at index " + scrollIndex);
|
||||
deleteContactMenuItem.setText("Delete " + user.getName());
|
||||
|
||||
// Read the current chat
|
||||
currentChat.read(writeProxy);
|
||||
@ -363,20 +363,28 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
remainingChars
|
||||
.setText(String.format("remaining chars: %d/%d", MAX_MESSAGE_LENGTH - messageTextArea.getText().length(), MAX_MESSAGE_LENGTH));
|
||||
}
|
||||
messageTextArea.setDisable(currentChat == null || postingPermanentlyDisabled);
|
||||
voiceButton.setDisable(!recorder.isSupported());
|
||||
attachmentButton.setDisable(false);
|
||||
|
||||
// Enable or disable the necessary UI controls
|
||||
final var chatEditable = currentChat == null || currentChat.isDisabled();
|
||||
messageTextArea.setDisable(chatEditable || postingPermanentlyDisabled);
|
||||
voiceButton.setDisable(!recorder.isSupported() || chatEditable);
|
||||
attachmentButton.setDisable(chatEditable);
|
||||
chatList.refresh();
|
||||
|
||||
// Design the top bar
|
||||
if (currentChat != null) {
|
||||
topBarContactLabel.setText(currentChat.getRecipient().getName());
|
||||
topBarContactLabel.setVisible(true);
|
||||
topBarStatusLabel.setVisible(true);
|
||||
if (currentChat.getRecipient() instanceof User) {
|
||||
final var status = ((User) currentChat.getRecipient()).getStatus().toString();
|
||||
topBarStatusLabel.setText(status);
|
||||
topBarStatusLabel.getStyleClass().clear();
|
||||
topBarStatusLabel.getStyleClass().add(status.toLowerCase());
|
||||
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("user_icon", 43));
|
||||
} else {
|
||||
topBarStatusLabel.setText(currentChat.getRecipient().getContacts().size() + " members");
|
||||
topBarStatusLabel.setText(currentChat.getRecipient().getContacts().size() + " member"
|
||||
+ (currentChat.getRecipient().getContacts().size() != 1 ? "s" : ""));
|
||||
topBarStatusLabel.getStyleClass().clear();
|
||||
recipientProfilePic.setImage(IconUtil.loadIconThemeSensitive("group_icon", 43));
|
||||
}
|
||||
@ -386,7 +394,6 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
clip.setArcHeight(43);
|
||||
clip.setArcWidth(43);
|
||||
recipientProfilePic.setClip(clip);
|
||||
|
||||
messageSearchButton.setVisible(true);
|
||||
}
|
||||
}
|
||||
@ -665,9 +672,9 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
Platform.runLater(() -> {
|
||||
chats.getSource().remove(currentChat);
|
||||
((ObservableList<Chat>) chats.getSource()).add(0, currentChat);
|
||||
chatList.getSelectionModel().select(0);
|
||||
localDB.getChats().remove(currentChat);
|
||||
localDB.getChats().add(0, currentChat);
|
||||
chatList.getSelectionModel().select(0);
|
||||
});
|
||||
scrollToMessageListEnd();
|
||||
|
||||
@ -712,7 +719,8 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
* @since Envoy Client v0.1-beta
|
||||
*/
|
||||
private void updateAttachmentView(boolean visible) {
|
||||
if (!attachmentView.getImage().equals(DEFAULT_ATTACHMENT_VIEW_IMAGE)) attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
|
||||
if (!(attachmentView.getImage() == null || attachmentView.getImage().equals(DEFAULT_ATTACHMENT_VIEW_IMAGE)))
|
||||
attachmentView.setImage(DEFAULT_ATTACHMENT_VIEW_IMAGE);
|
||||
attachmentView.setVisible(visible);
|
||||
}
|
||||
|
||||
@ -727,14 +735,59 @@ public final class ChatScene implements EventListener, Restorable {
|
||||
// Else prepend it to the HBox children
|
||||
final var ownUserControl = new ContactControl(localDB.getUser());
|
||||
ownUserControl.setAlignment(Pos.CENTER_LEFT);
|
||||
HBox.setHgrow(ownUserControl, Priority.NEVER);
|
||||
ownContactControl.getChildren().add(0, ownUserControl);
|
||||
}
|
||||
}
|
||||
|
||||
// Context menu actions
|
||||
/**
|
||||
* Redesigns the UI when the {@link Chat} of the given contact has been marked
|
||||
* as disabled.
|
||||
*
|
||||
* @param event the contact whose chat got disabled
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
@Event
|
||||
public void disableChat(ContactDisabled event) {
|
||||
chatList.refresh();
|
||||
final var recipient = event.get();
|
||||
|
||||
@FXML
|
||||
private void deleteContact() { try {} catch (final NullPointerException e) {} }
|
||||
// Decrement member count for groups
|
||||
if (recipient instanceof Group)
|
||||
topBarStatusLabel.setText(recipient.getContacts().size() + " member" + (recipient.getContacts().size() != 1 ? "s" : ""));
|
||||
if (currentChat != null && currentChat.getRecipient().equals(recipient)) {
|
||||
messageTextArea.setDisable(true);
|
||||
voiceButton.setDisable(true);
|
||||
attachmentButton.setDisable(true);
|
||||
pendingAttachment = null;
|
||||
messageList.getStyleClass().clear();
|
||||
messageList.getStyleClass().add("disabled-chat");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets every component back to its inital state before a chat was selected.
|
||||
*
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public void resetState() {
|
||||
currentChat = null;
|
||||
chatList.getSelectionModel().clearSelection();
|
||||
messageList.getItems().clear();
|
||||
messageTextArea.setDisable(true);
|
||||
attachmentView.setImage(null);
|
||||
topBarContactLabel.setVisible(false);
|
||||
topBarStatusLabel.setVisible(false);
|
||||
messageSearchButton.setVisible(false);
|
||||
messageTextArea.clear();
|
||||
messageTextArea.setDisable(true);
|
||||
attachmentButton.setDisable(true);
|
||||
voiceButton.setDisable(true);
|
||||
remainingChars.setVisible(false);
|
||||
pendingAttachment = null;
|
||||
recipientProfilePic.setImage(null);
|
||||
if (recorder.isRecording()) recorder.cancel();
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void copyAndPostMessage() {
|
||||
|
@ -63,7 +63,7 @@ public class ContactSearchTab implements EventListener {
|
||||
}
|
||||
|
||||
@Event
|
||||
private void onContactOperation(ContactOperation operation) {
|
||||
private void onUserOperation(UserOperation operation) {
|
||||
final var contact = operation.get();
|
||||
if (operation.getOperationType() == ElementOperation.ADD) Platform.runLater(() -> {
|
||||
userList.getItems().remove(contact);
|
||||
@ -96,7 +96,7 @@ public class ContactSearchTab implements EventListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an {@link ContactOperation} for the selected user to the
|
||||
* Sends an {@link UserOperation} for the selected user to the
|
||||
* server.
|
||||
*
|
||||
* @since Envoy Client v0.1-beta
|
||||
@ -114,7 +114,7 @@ public class ContactSearchTab implements EventListener {
|
||||
private void addAsContact() {
|
||||
|
||||
// Sends the event to the server
|
||||
final var event = new ContactOperation(currentlySelectedUser, ElementOperation.ADD);
|
||||
final var event = new UserOperation(currentlySelectedUser, ElementOperation.ADD);
|
||||
client.send(event);
|
||||
|
||||
// Removes the chosen user and updates the UI
|
||||
@ -124,5 +124,8 @@ public class ContactSearchTab implements EventListener {
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void backButtonClicked() { eventBus.dispatch(new BackEvent()); }
|
||||
private void backButtonClicked() {
|
||||
searchBar.setText("");
|
||||
eventBus.dispatch(new BackEvent());
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import envoy.client.ui.control.*;
|
||||
import envoy.client.ui.listcell.ListCellFactory;
|
||||
import envoy.data.*;
|
||||
import envoy.event.GroupCreation;
|
||||
import envoy.event.contact.ContactOperation;
|
||||
import envoy.event.contact.UserOperation;
|
||||
import envoy.util.Bounds;
|
||||
|
||||
import dev.kske.eventbus.*;
|
||||
@ -82,7 +82,7 @@ public class GroupCreationTab implements EventListener {
|
||||
.map(User.class::cast)
|
||||
.collect(Collectors.toList()));
|
||||
resizeQuickSelectSpace(0);
|
||||
quickSelectList.addEventFilter(MouseEvent.MOUSE_PRESSED, evt -> evt.consume());
|
||||
quickSelectList.addEventFilter(MouseEvent.MOUSE_PRESSED, MouseEvent::consume);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -234,11 +234,11 @@ public class GroupCreationTab implements EventListener {
|
||||
}
|
||||
|
||||
@Event
|
||||
private void onContactOperation(ContactOperation operation) {
|
||||
if (operation.get() instanceof User) Platform.runLater(() -> {
|
||||
private void onUserOperation(UserOperation operation) {
|
||||
Platform.runLater(() -> {
|
||||
switch (operation.getOperationType()) {
|
||||
case ADD:
|
||||
userList.getItems().add((User) operation.get());
|
||||
userList.getItems().add(operation.get());
|
||||
break;
|
||||
case REMOVE:
|
||||
userList.getItems().removeIf(operation.get()::equals);
|
||||
|
@ -33,6 +33,7 @@ public abstract class AbstractListCell<T, U extends Node> extends ListCell<T> {
|
||||
setGraphic(renderItem(item));
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setCursor(Cursor.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,46 @@
|
||||
package envoy.client.ui.listcell;
|
||||
|
||||
import javafx.scene.control.*;
|
||||
|
||||
import envoy.client.data.*;
|
||||
import envoy.client.net.Client;
|
||||
import envoy.client.ui.control.ChatControl;
|
||||
import envoy.client.util.UserUtil;
|
||||
import envoy.data.User;
|
||||
|
||||
/**
|
||||
* A list cell containing chats represented as chat controls.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public class ChatListCell extends AbstractListCell<Chat, ChatControl> {
|
||||
|
||||
private static final Client client = Context.getInstance().getClient();
|
||||
|
||||
/**
|
||||
* @param listView the list view inside of which the cell will be displayed
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public ChatListCell(ListView<? extends Chat> listView) { super(listView); }
|
||||
|
||||
@Override
|
||||
protected ChatControl renderItem(Chat chat) {
|
||||
if (client.isOnline()) {
|
||||
final var menu = new ContextMenu();
|
||||
final var removeMI = new MenuItem();
|
||||
removeMI.setText(
|
||||
chat.isDisabled() ? "Delete " : chat.getRecipient() instanceof User ? "Block " : "Leave group " + chat.getRecipient().getName());
|
||||
removeMI.setOnAction(
|
||||
chat.isDisabled() ? e -> UserUtil.deleteContact(chat.getRecipient()) : e -> UserUtil.disableContact(chat.getRecipient()));
|
||||
menu.getItems().add(removeMI);
|
||||
setContextMenu(menu);
|
||||
} else setContextMenu(null);
|
||||
|
||||
// TODO: replace with icon in ChatControl
|
||||
final var chatControl = new ChatControl(chat);
|
||||
if (chat.isDisabled()) chatControl.getStyleClass().add("disabled-chat");
|
||||
else chatControl.getStyleClass().remove("disabled-chat");
|
||||
return chatControl;
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package envoy.client.util;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.*;
|
||||
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Alert.AlertType;
|
||||
@ -9,8 +9,11 @@ import envoy.client.data.Context;
|
||||
import envoy.client.event.*;
|
||||
import envoy.client.helper.*;
|
||||
import envoy.client.ui.SceneContext.SceneInfo;
|
||||
import envoy.client.ui.controller.ChatScene;
|
||||
import envoy.data.*;
|
||||
import envoy.data.User.UserStatus;
|
||||
import envoy.event.UserStatusChange;
|
||||
import envoy.event.*;
|
||||
import envoy.event.contact.UserOperation;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
import dev.kske.eventbus.EventBus;
|
||||
@ -23,6 +26,9 @@ import dev.kske.eventbus.EventBus;
|
||||
*/
|
||||
public final class UserUtil {
|
||||
|
||||
private static final Context context = Context.getInstance();
|
||||
private static final Logger logger = EnvoyLog.getLogger(UserUtil.class);
|
||||
|
||||
private UserUtil() {}
|
||||
|
||||
/**
|
||||
@ -40,7 +46,8 @@ public final class UserUtil {
|
||||
EnvoyLog.getLogger(ShutdownHelper.class).log(Level.INFO, "A logout was requested");
|
||||
EventBus.getInstance().dispatch(new EnvoyCloseEvent());
|
||||
EventBus.getInstance().dispatch(new Logout());
|
||||
Context.getInstance().getSceneContext().load(SceneInfo.LOGIN_SCENE);
|
||||
context.getSceneContext().load(SceneInfo.LOGIN_SCENE);
|
||||
logger.log(Level.INFO, "A logout occurred.");
|
||||
});
|
||||
}
|
||||
|
||||
@ -54,11 +61,56 @@ public final class UserUtil {
|
||||
public static void changeStatus(UserStatus newStatus) {
|
||||
|
||||
// Sending the already active status is a valid action
|
||||
if (newStatus.equals(Context.getInstance().getLocalDB().getUser().getStatus())) return;
|
||||
if (newStatus.equals(context.getLocalDB().getUser().getStatus())) return;
|
||||
else {
|
||||
EventBus.getInstance().dispatch(new OwnStatusChange(newStatus));
|
||||
if (Context.getInstance().getClient().isOnline())
|
||||
Context.getInstance().getClient().send(new UserStatusChange(Context.getInstance().getLocalDB().getUser().getID(), newStatus));
|
||||
if (context.getClient().isOnline()) context.getClient().send(new UserStatusChange(context.getLocalDB().getUser().getID(), newStatus));
|
||||
logger.log(Level.INFO, "A manual status change occurred.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given contact.
|
||||
*
|
||||
* @param block the contact that should be removed
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public static void disableContact(Contact block) {
|
||||
if (!context.getClient().isOnline() || block == null) return;
|
||||
else {
|
||||
final var alert = new Alert(AlertType.CONFIRMATION);
|
||||
alert.setContentText("Are you sure you want to " + (block instanceof User ? "block " : "leave group ") + block.getName() + "?");
|
||||
AlertHelper.confirmAction(alert, () -> {
|
||||
final var isUser = block instanceof User;
|
||||
context.getClient()
|
||||
.send(isUser ? new UserOperation((User) block, ElementOperation.REMOVE)
|
||||
: new GroupResize(context.getLocalDB().getUser(), (Group) block, ElementOperation.REMOVE));
|
||||
if (!isUser) block.getContacts().remove(context.getLocalDB().getUser());
|
||||
EventBus.getInstance().dispatch(new ContactDisabled(block));
|
||||
logger.log(Level.INFO, isUser ? "A user was blocked." : "The user left a group.");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given contact with all his messages entirely.
|
||||
*
|
||||
delvh marked this conversation as resolved
kske
commented
Use an event here instead. This would also simplify the interaction with the local database. Use an event here instead. This would also simplify the interaction with the local database.
|
||||
* @param delete the contact to delete
|
||||
* @since Envoy Client v0.3-beta
|
||||
*/
|
||||
public static void deleteContact(Contact delete) {
|
||||
if (delete == null) return;
|
||||
else {
|
||||
final var alert = new Alert(AlertType.CONFIRMATION);
|
||||
alert.setContentText("Are you sure you want to delete " + delete.getName()
|
||||
+ " entirely? All messages with this contact will be deleted. This action cannot be undone.");
|
||||
AlertHelper.confirmAction(alert, () -> {
|
||||
context.getLocalDB().getUsers().remove(delete.getName());
|
||||
context.getLocalDB().getChats().removeIf(chat -> chat.getRecipient().equals(delete));
|
||||
if (context.getSceneContext().getController() instanceof ChatScene)
|
||||
((ChatScene) context.getSceneContext().getController()).resetState();
|
||||
logger.log(Level.INFO, "A contact with all his messages was deleted.");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -140,19 +140,24 @@
|
||||
.tab-pane {
|
||||
-fx-tab-max-height: 0.0 ;
|
||||
}
|
||||
|
||||
.tab-pane .tab-header-area {
|
||||
visibility: hidden ;
|
||||
-fx-padding: -20.0 0.0 0.0 0.0;
|
||||
}
|
||||
|
||||
.disabled-chat {
|
||||
-fx-background-color: #0000FF;
|
||||
}
|
||||
|
||||
#quick-select-list .scroll-bar:horizontal{
|
||||
-fx-pref-height: 0;
|
||||
-fx-max-height: 0;
|
||||
-fx-min-height: 0;
|
||||
-fx-pref-height: 0.0;
|
||||
-fx-max-height: 0.0;
|
||||
-fx-min-height: 0.0;
|
||||
}
|
||||
|
||||
#quick-select-list .scroll-bar:vertical{
|
||||
-fx-pref-width: 0;
|
||||
-fx-max-width: 0;
|
||||
-fx-min-width: 0;
|
||||
-fx-pref-width: 0.0;
|
||||
-fx-max-width: 0.0;
|
||||
-fx-min-width: 0.0;
|
||||
}
|
||||
|
@ -126,15 +126,6 @@
|
||||
<ListView id="chat-list" fx:id="chatList"
|
||||
focusTraversable="false" onMouseClicked="#chatListClicked"
|
||||
prefWidth="316.0" VBox.vgrow="ALWAYS">
|
||||
<contextMenu>
|
||||
<ContextMenu anchorLocation="CONTENT_TOP_LEFT">
|
||||
<items>
|
||||
<MenuItem fx:id="deleteContactMenuItem"
|
||||
mnemonicParsing="false" onAction="#deleteContact"
|
||||
text="Delete" />
|
||||
</items>
|
||||
</ContextMenu>
|
||||
</contextMenu>
|
||||
<padding>
|
||||
<Insets bottom="5.0" left="5.0" right="2.0" top="5.0" />
|
||||
</padding>
|
||||
@ -167,7 +158,7 @@
|
||||
<HBox id="transparent-background" fx:id="ownContactControl">
|
||||
<children>
|
||||
<Region id="transparent-background" prefWidth="120"
|
||||
fx:id="spaceBetweenUserAndSettingsButton" />
|
||||
fx:id="spaceBetweenUserAndSettingsButton" HBox.hgrow="ALWAYS" />
|
||||
<Button fx:id="settingsButton" mnemonicParsing="false"
|
||||
onAction="#settingsButtonClicked" prefHeight="30.0"
|
||||
prefWidth="30.0" text="" alignment="CENTER">
|
||||
|
@ -17,14 +17,14 @@ public final class GroupCreation extends Event<String> {
|
||||
private static final long serialVersionUID = 0L;
|
||||
|
||||
/**
|
||||
* @param value the name of this group at creation time
|
||||
* @param name the name of this group at creation time
|
||||
* @param initialMemberIDs the IDs of all {@link User}s that should be group
|
||||
* members from the beginning on (excluding the creator
|
||||
* of this group)
|
||||
* @since Envoy Common v0.1-beta
|
||||
*/
|
||||
public GroupCreation(String value, Set<Long> initialMemberIDs) {
|
||||
super(value);
|
||||
public GroupCreation(String name, Set<Long> initialMemberIDs) {
|
||||
super(name);
|
||||
this.initialMemberIDs = initialMemberIDs != null ? initialMemberIDs : new HashSet<>();
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
package envoy.event;
|
||||
|
||||
import envoy.data.Group;
|
||||
|
||||
/**
|
||||
* Used to communicate with a client that his request to create a group might
|
||||
* have been rejected as it might be disabled on his current server.
|
||||
@ -7,15 +9,23 @@ package envoy.event;
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Common v0.2-beta
|
||||
*/
|
||||
public class GroupCreationResult extends Event<Boolean> {
|
||||
public class GroupCreationResult extends Event<Group> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Creates a new {@code GroupCreationResult}.
|
||||
* Creates a new {@code GroupCreationResult} that implies the failure of this
|
||||
* {@link GroupCreationResult}.
|
||||
*
|
||||
* @param success whether the GroupCreation sent before was successful
|
||||
* @since Envoy Common v0.2-beta
|
||||
*/
|
||||
public GroupCreationResult(boolean success) { super(success); }
|
||||
public GroupCreationResult() { super(null); }
|
||||
|
||||
/**
|
||||
* Creates a new {@code GroupCreationResult}.
|
||||
*
|
||||
* @param resultGroup the group the server created
|
||||
* @since Envoy Common v0.2-beta
|
||||
*/
|
||||
public GroupCreationResult(Group resultGroup) { super(resultGroup); }
|
||||
}
|
||||
|
@ -34,11 +34,10 @@ public final class GroupResize extends Event<User> {
|
||||
public GroupResize(User user, Group group, ElementOperation operation) {
|
||||
super(user);
|
||||
this.operation = operation;
|
||||
if (group.getContacts().contains(user)) {
|
||||
if (operation.equals(ADD))
|
||||
final var contained = group.getContacts().contains(user);
|
||||
if (contained && operation.equals(ADD))
|
||||
throw new IllegalArgumentException(String.format("Cannot add %s to %s!", user, group));
|
||||
} else if (operation.equals(REMOVE))
|
||||
throw new IllegalArgumentException(String.format("Cannot remove %s from %s!", user, group));
|
||||
else if (operation.equals(REMOVE) && !contained) throw new IllegalArgumentException(String.format("Cannot remove %s from %s!", user, group));
|
||||
groupID = group.getID();
|
||||
}
|
||||
|
||||
@ -72,5 +71,5 @@ public final class GroupResize extends Event<User> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() { return String.format("GroupResize[userid=%d,groupid=%d,operation=%s]", get(), groupID, operation); }
|
||||
public String toString() { return String.format("GroupResize[user=%s,groupid=%d,operation=%s]", get(), groupID, operation); }
|
||||
}
|
||||
|
@ -0,0 +1,15 @@
|
||||
package envoy.event.contact;
|
||||
|
||||
import envoy.event.Event.Valueless;
|
||||
|
||||
/**
|
||||
* Conveys that either a direct contact or a group member has been deleted while
|
||||
* the user has been offline.
|
||||
*
|
||||
* @author Leon Hofmeister
|
||||
* @since Envoy Common v0.3-beta
|
||||
*/
|
||||
public class ContactsChangedSinceLastLogin extends Valueless {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -1,35 +1,38 @@
|
||||
package envoy.event.contact;
|
||||
|
||||
import envoy.data.Contact;
|
||||
import envoy.data.User;
|
||||
import envoy.event.*;
|
||||
|
||||
/**
|
||||
* Signifies the modification of a contact list.
|
||||
*
|
||||
* @author Maximilian Käfer
|
||||
* @since Envoy Common v0.2-alpha
|
||||
* @since Envoy Common v0.3-beta
|
||||
*/
|
||||
public final class ContactOperation extends Event<Contact> {
|
||||
public final class UserOperation extends Event<User> {
|
||||
|
||||
private final ElementOperation operationType;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* Initializes a {@link ContactOperation}.
|
||||
* Initializes a {@link UserOperation}.
|
||||
*
|
||||
* @param contact the user on which the operation is performed
|
||||
* @param operationType the type of operation to perform
|
||||
* @since Envoy Common v0.2-alpha
|
||||
* @since Envoy Common v0.3-beta
|
||||
*/
|
||||
public ContactOperation(Contact contact, ElementOperation operationType) {
|
||||
public UserOperation(User contact, ElementOperation operationType) {
|
||||
super(contact);
|
||||
this.operationType = operationType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the type of operation to perform
|
||||
* @since Envoy Common v0.2-alpha
|
||||
* @since Envoy Common v0.3-beta
|
||||
*/
|
||||
public ElementOperation getOperationType() { return operationType; }
|
||||
|
||||
@Override
|
||||
public String toString() { return String.format("%s[contact=%s, operation=%s]", UserOperation.class.getSimpleName(), value, operationType); }
|
||||
}
|
@ -49,9 +49,10 @@ public final class Startup {
|
||||
new MessageStatusChangeProcessor(),
|
||||
new GroupMessageStatusChangeProcessor(),
|
||||
new UserStatusChangeProcessor(),
|
||||
new GroupResizeProcessor(),
|
||||
new IDGeneratorRequestProcessor(),
|
||||
new UserSearchProcessor(),
|
||||
new ContactOperationProcessor(),
|
||||
new UserOperationProcessor(),
|
||||
new IsTypingProcessor(),
|
||||
new NameChangeProcessor(),
|
||||
new ProfilePicChangeProcessor(),
|
||||
|
@ -51,11 +51,11 @@ public class Message {
|
||||
@Id
|
||||
protected long id;
|
||||
|
||||
@ManyToOne
|
||||
@ManyToOne(cascade = CascadeType.REMOVE)
|
||||
@JoinColumn
|
||||
protected User sender;
|
||||
|
||||
@ManyToOne
|
||||
@ManyToOne(cascade = CascadeType.REMOVE)
|
||||
@JoinColumn
|
||||
protected Contact recipient;
|
||||
|
||||
|
@ -2,11 +2,13 @@ package envoy.server.data;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import javax.persistence.*;
|
||||
|
||||
import envoy.data.User.UserStatus;
|
||||
import envoy.server.net.ConnectionManager;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* Contains operations used for persistence.
|
||||
@ -97,7 +99,15 @@ public final class PersistenceManager {
|
||||
* @param contact the {@link Contact} to delete
|
||||
* @since Envoy Server Standalone v0.1-alpha
|
||||
*/
|
||||
public void deleteContact(Contact contact) { remove(contact); }
|
||||
public void deleteContact(Contact contact) {
|
||||
transaction(() -> {
|
||||
|
||||
// Remove this contact from the contact list of his contacts
|
||||
for (final var remainingContact : contact.getContacts())
|
||||
remainingContact.getContacts().remove(contact);
|
||||
});
|
||||
remove(contact);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a {@link Message} in the database.
|
||||
@ -227,17 +237,52 @@ public final class PersistenceManager {
|
||||
* @since Envoy Server Standalone v0.1-alpha
|
||||
*/
|
||||
public void addContactBidirectional(long contactID1, long contactID2) {
|
||||
addContactBidirectional(getContactByID(contactID1), getContactByID(contactID2));
|
||||
}
|
||||
|
||||
// Get users by ID
|
||||
final var c1 = getContactByID(contactID1);
|
||||
final var c2 = getContactByID(contactID2);
|
||||
/**
|
||||
* Adds a contact to the contact list of another contact and vice versa.
|
||||
*
|
||||
* @param contact1 the first contact
|
||||
* @param contact2 the second contact
|
||||
* @since Envoy Server v0.3-beta
|
||||
*/
|
||||
public void addContactBidirectional(Contact contact1, Contact contact2) {
|
||||
|
||||
// Add users to each others contact lists
|
||||
c1.getContacts().add(c2);
|
||||
c2.getContacts().add(c1);
|
||||
// Add users to each others contact list
|
||||
contact1.getContacts().add(contact2);
|
||||
contact2.getContacts().add(contact1);
|
||||
|
||||
// Synchronize changes with the database
|
||||
transaction(() -> { entityManager.merge(c1); entityManager.merge(c2); });
|
||||
transaction(() -> { entityManager.merge(contact1); entityManager.merge(contact2); });
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a contact from the contact list of another contact and vice versa.
|
||||
*
|
||||
* @param contactID1 the ID of the first contact
|
||||
* @param contactID2 the ID of the second contact
|
||||
* @since Envoy Server v0.3-beta
|
||||
*/
|
||||
public void removeContactBidirectional(long contactID1, long contactID2) {
|
||||
removeContactBidirectional(getContactByID(contactID1), getContactByID(contactID2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a contact from the contact list of another contact and vice versa.
|
||||
*
|
||||
* @param contact1 the first contact
|
||||
* @param contact2 the second contact
|
||||
* @since Envoy Server v0.3-beta
|
||||
*/
|
||||
public void removeContactBidirectional(Contact contact1, Contact contact2) {
|
||||
|
||||
// Remove users from each others contact list
|
||||
contact1.getContacts().remove(contact2);
|
||||
contact2.getContacts().remove(contact1);
|
||||
|
||||
// Synchronize changes with the database
|
||||
transaction(() -> { entityManager.merge(contact1); entityManager.merge(contact2); });
|
||||
}
|
||||
|
||||
/**
|
||||
@ -277,6 +322,14 @@ public final class PersistenceManager {
|
||||
entityManagerRelatedAction.run();
|
||||
transaction.commit();
|
||||
}
|
||||
} catch (final RollbackException e2) {
|
||||
|
||||
// Apparently a major problem exists. Discard faulty transaction and then go on.
|
||||
if (transaction.isActive()) {
|
||||
transaction.rollback();
|
||||
EnvoyLog.getLogger(PersistenceManager.class)
|
||||
.log(Level.SEVERE, "Could not perform transaction, hence discarding it. It's likely that a serious issue exists.");
|
||||
} else throw e2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,6 +70,9 @@ public final class User extends Contact {
|
||||
@Column(name = "last_seen")
|
||||
private Instant lastSeen;
|
||||
|
||||
@Column(name = "latest_contact_deletion")
|
||||
private Instant latestContactDeletion;
|
||||
|
||||
private UserStatus status;
|
||||
|
||||
@Override
|
||||
@ -140,4 +143,16 @@ public final class User extends Contact {
|
||||
* @since Envoy Server Standalone v0.1-alpha
|
||||
*/
|
||||
public void setStatus(UserStatus status) { this.status = status; }
|
||||
|
||||
/**
|
||||
* @return the latestContactDeletion
|
||||
* @since Envoy Server v0.3-beta
|
||||
*/
|
||||
public Instant getLatestContactDeletion() { return latestContactDeletion; }
|
||||
|
||||
/**
|
||||
* @param latestContactDeletion the latestContactDeletion to set
|
||||
* @since Envoy Server v0.3-beta
|
||||
*/
|
||||
public void setLatestContactDeletion(Instant latestContactDeletion) { this.latestContactDeletion = latestContactDeletion; }
|
||||
}
|
||||
|
@ -1,39 +0,0 @@
|
||||
package envoy.server.processors;
|
||||
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import envoy.event.ElementOperation;
|
||||
import envoy.event.contact.ContactOperation;
|
||||
import envoy.server.data.PersistenceManager;
|
||||
import envoy.server.net.*;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Server Standalone v0.1-alpha
|
||||
*/
|
||||
public final class ContactOperationProcessor implements ObjectProcessor<ContactOperation> {
|
||||
|
||||
private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
|
||||
private static final Logger logger = EnvoyLog.getLogger(ContactOperationProcessor.class);
|
||||
|
||||
@Override
|
||||
public void process(ContactOperation evt, long socketId, ObjectWriteProxy writeProxy) {
|
||||
switch (evt.getOperationType()) {
|
||||
case ADD:
|
||||
final long userID = ConnectionManager.getInstance().getUserIDBySocketID(socketId);
|
||||
final long contactId = evt.get().getID();
|
||||
|
||||
logger.fine(String.format("Adding user %s to the contact list of user %d.%n", evt.get(), userID));
|
||||
PersistenceManager.getInstance().addContactBidirectional(userID, contactId);
|
||||
|
||||
// Notify the contact if online
|
||||
if (ConnectionManager.getInstance().isOnline(contactId))
|
||||
writeProxy.write(connectionManager.getSocketID(contactId),
|
||||
new ContactOperation(PersistenceManager.getInstance().getUserByID(userID).toCommon(), ElementOperation.ADD));
|
||||
break;
|
||||
default:
|
||||
logger.warning(String.format("Received %s with an unsupported operation.", evt));
|
||||
}
|
||||
}
|
||||
}
|
@ -5,8 +5,7 @@ import static envoy.server.Startup.config;
|
||||
import java.util.HashSet;
|
||||
|
||||
import envoy.event.*;
|
||||
import envoy.event.contact.ContactOperation;
|
||||
import envoy.server.data.*;
|
||||
import envoy.server.data.PersistenceManager;
|
||||
import envoy.server.net.*;
|
||||
|
||||
/**
|
||||
@ -21,21 +20,19 @@ public final class GroupCreationProcessor implements ObjectProcessor<GroupCreati
|
||||
@Override
|
||||
public void process(GroupCreation groupCreation, long socketID, ObjectWriteProxy writeProxy) {
|
||||
// Don't allow the creation of groups if manually disabled
|
||||
writeProxy.write(socketID, new GroupCreationResult(config.isGroupSupportEnabled()));
|
||||
if (!config.isGroupSupportEnabled()) return;
|
||||
if (!config.isGroupSupportEnabled()) {
|
||||
writeProxy.write(socketID, new GroupCreationResult());
|
||||
return;
|
||||
}
|
||||
final envoy.server.data.Group group = new envoy.server.data.Group();
|
||||
group.setName(groupCreation.get());
|
||||
group.setContacts(new HashSet<>());
|
||||
groupCreation.getInitialMemberIDs().stream().map(persistenceManager::getUserByID).forEach(group.getContacts()::add);
|
||||
group.getContacts().add(persistenceManager.getContactByID(connectionManager.getUserIDBySocketID(socketID)));
|
||||
group.getContacts().forEach(c -> c.getContacts().add(group));
|
||||
group.getContacts().add(persistenceManager.getUserByID(connectionManager.getUserIDBySocketID(socketID)));
|
||||
persistenceManager.addContact(group);
|
||||
group.getContacts()
|
||||
groupCreation.getInitialMemberIDs()
|
||||
.stream()
|
||||
.map(Contact::getID)
|
||||
.filter(connectionManager::isOnline)
|
||||
.map(connectionManager::getSocketID)
|
||||
.forEach(memberSocketID -> writeProxy.write(memberSocketID, new ContactOperation(group.toCommon(), ElementOperation.ADD)));
|
||||
.map(persistenceManager::getUserByID)
|
||||
.forEach(member -> persistenceManager.addContactBidirectional(member, group));
|
||||
persistenceManager.addContactBidirectional(persistenceManager.getUserByID(connectionManager.getUserIDBySocketID(socketID)), group);
|
||||
writeProxy.writeToOnlineContacts(group.getContacts(), new GroupCreationResult(group.toCommon()));
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
package envoy.server.processors;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import envoy.event.GroupResize;
|
||||
import envoy.server.data.*;
|
||||
import envoy.server.net.*;
|
||||
import envoy.server.net.ObjectWriteProxy;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* @author Maximilian Käfer
|
||||
@ -11,34 +15,37 @@ import envoy.server.net.*;
|
||||
public final class GroupResizeProcessor implements ObjectProcessor<GroupResize> {
|
||||
|
||||
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
|
||||
private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
|
||||
|
||||
@Override
|
||||
public void process(GroupResize groupResize, long socketID, ObjectWriteProxy writeProxy) {
|
||||
|
||||
// Acquire the group to resize from the database
|
||||
var group = persistenceManager.getGroupByID(groupResize.getGroupID());
|
||||
final var group = persistenceManager.getGroupByID(groupResize.getGroupID());
|
||||
final var sender = persistenceManager.getUserByID(groupResize.get().getID());
|
||||
|
||||
// Perform the desired operation
|
||||
switch (groupResize.getOperation()) {
|
||||
case ADD:
|
||||
group.getContacts().add(persistenceManager.getUserByID(groupResize.get().getID()));
|
||||
break;
|
||||
persistenceManager.addContactBidirectional(sender, group);
|
||||
writeProxy.writeToOnlineContacts(group.getContacts(), group.toCommon());
|
||||
return;
|
||||
case REMOVE:
|
||||
group.getContacts().remove(persistenceManager.getUserByID(groupResize.get().getID()));
|
||||
break;
|
||||
persistenceManager.removeContactBidirectional(sender, group);
|
||||
sender.setLatestContactDeletion(Instant.now());
|
||||
|
||||
// The group has no more members and hence will be deleted
|
||||
if (group.getContacts().isEmpty()) {
|
||||
EnvoyLog.getLogger(GroupResizeProcessor.class).log(Level.INFO, "Deleting now empty group " + group.getName());
|
||||
persistenceManager.deleteContact(group);
|
||||
} else {
|
||||
|
||||
// Informing the other members
|
||||
writeProxy.writeToOnlineContacts(group.getContacts(), groupResize);
|
||||
group.getContacts().forEach(c -> ((User) c).setLatestContactDeletion(Instant.now()));
|
||||
}
|
||||
|
||||
// Update the group in the database
|
||||
persistenceManager.updateContact(group);
|
||||
|
||||
// Send the updated group to all of its members
|
||||
var commonGroup = group.toCommon();
|
||||
group.getContacts()
|
||||
.stream()
|
||||
.map(Contact::getID)
|
||||
.filter(connectionManager::isOnline)
|
||||
.map(connectionManager::getSocketID)
|
||||
.forEach(memberSocketID -> writeProxy.write(memberSocketID, commonGroup));
|
||||
group.getContacts().remove(persistenceManager.getUserByID(groupResize.get().getID()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import javax.persistence.NoResultException;
|
||||
|
||||
import envoy.data.LoginCredentials;
|
||||
import envoy.event.*;
|
||||
import envoy.event.contact.ContactsChangedSinceLastLogin;
|
||||
import envoy.server.data.*;
|
||||
import envoy.server.net.*;
|
||||
import envoy.server.util.*;
|
||||
@ -104,6 +105,7 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
|
||||
user.setStatus(ONLINE);
|
||||
user.setPasswordHash(PasswordUtil.hash(credentials.getPassword()));
|
||||
user.setContacts(new HashSet<>());
|
||||
user.setLatestContactDeletion(Instant.EPOCH);
|
||||
persistenceManager.addContact(user);
|
||||
logger.info("Registered new " + user);
|
||||
}
|
||||
@ -205,6 +207,8 @@ public final class LoginCredentialProcessor implements ObjectProcessor<LoginCred
|
||||
writeProxy.write(socketID, new MessageStatusChange(gmsgCommon));
|
||||
}
|
||||
}
|
||||
// Notify the user if a contact deletion has happened since he last logged in
|
||||
if (user.getLatestContactDeletion().isAfter(user.getLastSeen())) writeProxy.write(socketID, new ContactsChangedSinceLastLogin());
|
||||
|
||||
// Complete the handshake
|
||||
writeProxy.write(socketID, user.toCommon());
|
||||
|
52
server/src/main/java/envoy/server/processors/UserOperationProcessor.java
Executable file
52
server/src/main/java/envoy/server/processors/UserOperationProcessor.java
Executable file
@ -0,0 +1,52 @@
|
||||
package envoy.server.processors;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.logging.*;
|
||||
|
||||
import envoy.event.ElementOperation;
|
||||
import envoy.event.contact.UserOperation;
|
||||
import envoy.server.data.PersistenceManager;
|
||||
import envoy.server.net.*;
|
||||
import envoy.util.EnvoyLog;
|
||||
|
||||
/**
|
||||
* @author Kai S. K. Engelbart
|
||||
* @since Envoy Server Standalone v0.1-alpha
|
||||
*/
|
||||
public final class UserOperationProcessor implements ObjectProcessor<UserOperation> {
|
||||
|
||||
private static final ConnectionManager connectionManager = ConnectionManager.getInstance();
|
||||
private static final Logger logger = EnvoyLog.getLogger(UserOperationProcessor.class);
|
||||
private static final PersistenceManager persistenceManager = PersistenceManager.getInstance();
|
||||
|
||||
@Override
|
||||
public void process(UserOperation evt, long socketId, ObjectWriteProxy writeProxy) {
|
||||
final long userID = ConnectionManager.getInstance().getUserIDBySocketID(socketId);
|
||||
final long contactID = evt.get().getID();
|
||||
final var sender = persistenceManager.getUserByID(userID);
|
||||
switch (evt.getOperationType()) {
|
||||
case ADD:
|
||||
logger.log(Level.FINE, String.format("Adding %s to the contact list of user %d.", evt.get(), userID));
|
||||
persistenceManager.addContactBidirectional(userID, contactID);
|
||||
|
||||
// Notify the contact if online
|
||||
if (connectionManager.isOnline(contactID))
|
||||
writeProxy.write(connectionManager.getSocketID(contactID), new UserOperation(sender.toCommon(), ElementOperation.ADD));
|
||||
break;
|
||||
case REMOVE:
|
||||
|
||||
// Remove the relationships and notify sender if he logs in using another
|
||||
// LocalDB
|
||||
persistenceManager.removeContactBidirectional(userID, contactID);
|
||||
sender.setLatestContactDeletion(Instant.now());
|
||||
|
||||
// Notify the removed contact on next startup(s) of this deletion
|
||||
persistenceManager.getUserByID(contactID).setLatestContactDeletion(Instant.now());
|
||||
|
||||
// Notify the removed contact if online
|
||||
if (connectionManager.isOnline(contactID))
|
||||
writeProxy.write(connectionManager.getSocketID(contactID), new UserOperation(sender.toCommon(), ElementOperation.REMOVE));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user
Why is this
private
even though we permit subclassing here?Because we don't need to access this in subclasses. Subclasses themselves should never disable themselves.
There is a public setter though. The chat can literally be disabled from anywhere.
According to Meyer's open-closed principle (the O in SOLID), software entities should open for extension, while closed for modification. To me, this implies that subclasses should be able to access this value without the use of a getter.
The problem I have with this is that in this case it is literally intended that they are modified from outside and that they do not modify themselves.