Merge pull request #164 from RecentRunner/implement-chat-notifications
implement chat notifications
This commit was merged in pull request #164.
This commit is contained in:
@@ -11,6 +11,7 @@ public class ConversationResponse {
|
|||||||
private String status;
|
private String status;
|
||||||
private String mode;
|
private String mode;
|
||||||
private String lastMessage;
|
private String lastMessage;
|
||||||
|
private Long lastSenderId;
|
||||||
private LocalDateTime humanRequestedAt;
|
private LocalDateTime humanRequestedAt;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
@@ -18,19 +19,20 @@ public class ConversationResponse {
|
|||||||
public ConversationResponse() {
|
public ConversationResponse() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ConversationResponse(Long id, Long customerId, Long staffId, String status, String mode, String lastMessage, LocalDateTime humanRequestedAt, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
public ConversationResponse(Long id, Long customerId, Long staffId, String status, String mode, String lastMessage, Long lastSenderId, LocalDateTime humanRequestedAt, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.customerId = customerId;
|
this.customerId = customerId;
|
||||||
this.staffId = staffId;
|
this.staffId = staffId;
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
this.lastMessage = lastMessage;
|
this.lastMessage = lastMessage;
|
||||||
|
this.lastSenderId = lastSenderId;
|
||||||
this.humanRequestedAt = humanRequestedAt;
|
this.humanRequestedAt = humanRequestedAt;
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ConversationResponse fromEntity(Conversation conversation, String lastMessage) {
|
public static ConversationResponse fromEntity(Conversation conversation, String lastMessage, Long lastSenderId) {
|
||||||
ConversationResponse response = new ConversationResponse();
|
ConversationResponse response = new ConversationResponse();
|
||||||
response.setId(conversation.getId());
|
response.setId(conversation.getId());
|
||||||
response.setCustomerId(conversation.getCustomerId());
|
response.setCustomerId(conversation.getCustomerId());
|
||||||
@@ -38,12 +40,21 @@ public class ConversationResponse {
|
|||||||
response.setStatus(conversation.getStatus().name());
|
response.setStatus(conversation.getStatus().name());
|
||||||
response.setMode(conversation.getMode().name());
|
response.setMode(conversation.getMode().name());
|
||||||
response.setLastMessage(lastMessage);
|
response.setLastMessage(lastMessage);
|
||||||
|
response.setLastSenderId(lastSenderId);
|
||||||
response.setHumanRequestedAt(conversation.getHumanRequestedAt());
|
response.setHumanRequestedAt(conversation.getHumanRequestedAt());
|
||||||
response.setCreatedAt(conversation.getCreatedAt());
|
response.setCreatedAt(conversation.getCreatedAt());
|
||||||
response.setUpdatedAt(conversation.getUpdatedAt());
|
response.setUpdatedAt(conversation.getUpdatedAt());
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getLastSenderId() {
|
||||||
|
return lastSenderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastSenderId(Long lastSenderId) {
|
||||||
|
this.lastSenderId = lastSenderId;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ public class MessageResponse {
|
|||||||
private Long id;
|
private Long id;
|
||||||
private Long conversationId;
|
private Long conversationId;
|
||||||
private Long senderId;
|
private Long senderId;
|
||||||
|
private String senderAvatarUrl;
|
||||||
private String content;
|
private String content;
|
||||||
private LocalDateTime timestamp;
|
private LocalDateTime timestamp;
|
||||||
private Boolean isRead;
|
private Boolean isRead;
|
||||||
@@ -19,10 +20,11 @@ public class MessageResponse {
|
|||||||
public MessageResponse() {
|
public MessageResponse() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageResponse(Long id, Long conversationId, Long senderId, String content, LocalDateTime timestamp, Boolean isRead) {
|
public MessageResponse(Long id, Long conversationId, Long senderId, String senderAvatarUrl, String content, LocalDateTime timestamp, Boolean isRead) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.conversationId = conversationId;
|
this.conversationId = conversationId;
|
||||||
this.senderId = senderId;
|
this.senderId = senderId;
|
||||||
|
this.senderAvatarUrl = senderAvatarUrl;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.timestamp = timestamp;
|
this.timestamp = timestamp;
|
||||||
this.isRead = isRead;
|
this.isRead = isRead;
|
||||||
@@ -48,6 +50,14 @@ public class MessageResponse {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSenderAvatarUrl() {
|
||||||
|
return senderAvatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSenderAvatarUrl(String senderAvatarUrl) {
|
||||||
|
this.senderAvatarUrl = senderAvatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,10 @@ public class ChatRealtimeService {
|
|||||||
Conversation conversation = conversationRepository.findById(conversationId)
|
Conversation conversation = conversationRepository.findById(conversationId)
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
||||||
List<com.petshop.backend.entity.Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
List<com.petshop.backend.entity.Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
||||||
String lastMessage = messages.isEmpty() ? "" : messages.get(messages.size() - 1).getContent();
|
com.petshop.backend.entity.Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1);
|
||||||
ConversationResponse response = ConversationResponse.fromEntity(conversation, lastMessage);
|
String lastMessage = last != null && last.getContent() != null ? last.getContent() : "";
|
||||||
|
Long lastSenderId = last != null ? last.getSenderId() : null;
|
||||||
|
ConversationResponse response = ConversationResponse.fromEntity(conversation, lastMessage, lastSenderId);
|
||||||
|
|
||||||
messagingTemplate.convertAndSend("/topic/chat/conversations", response);
|
messagingTemplate.convertAndSend("/topic/chat/conversations", response);
|
||||||
sendConversationToCustomerQueue(response);
|
sendConversationToCustomerQueue(response);
|
||||||
|
|||||||
@@ -28,15 +28,18 @@ public class ChatService {
|
|||||||
private final ConversationRepository conversationRepository;
|
private final ConversationRepository conversationRepository;
|
||||||
private final MessageRepository messageRepository;
|
private final MessageRepository messageRepository;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final AvatarStorageService avatarStorageService;
|
||||||
private final ChatAttachmentStorageService attachmentStorageService;
|
private final ChatAttachmentStorageService attachmentStorageService;
|
||||||
|
|
||||||
public ChatService(ConversationRepository conversationRepository,
|
public ChatService(ConversationRepository conversationRepository,
|
||||||
MessageRepository messageRepository,
|
MessageRepository messageRepository,
|
||||||
UserRepository userRepository,
|
UserRepository userRepository,
|
||||||
|
AvatarStorageService avatarStorageService,
|
||||||
ChatAttachmentStorageService attachmentStorageService) {
|
ChatAttachmentStorageService attachmentStorageService) {
|
||||||
this.conversationRepository = conversationRepository;
|
this.conversationRepository = conversationRepository;
|
||||||
this.messageRepository = messageRepository;
|
this.messageRepository = messageRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
|
this.avatarStorageService = avatarStorageService;
|
||||||
this.attachmentStorageService = attachmentStorageService;
|
this.attachmentStorageService = attachmentStorageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +65,7 @@ public class ChatService {
|
|||||||
message.setIsRead(false);
|
message.setIsRead(false);
|
||||||
messageRepository.save(message);
|
messageRepository.save(message);
|
||||||
|
|
||||||
return ConversationResponse.fromEntity(conversation, request.getMessage());
|
return ConversationResponse.fromEntity(conversation, request.getMessage(), userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ConversationResponse> getConversations(Long userId, User.Role role) {
|
public List<ConversationResponse> getConversations(Long userId, User.Role role) {
|
||||||
@@ -84,7 +87,8 @@ public class ChatService {
|
|||||||
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conv.getId());
|
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conv.getId());
|
||||||
Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1);
|
Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1);
|
||||||
String lastMessage = last != null && last.getContent() != null ? last.getContent() : "";
|
String lastMessage = last != null && last.getContent() != null ? last.getContent() : "";
|
||||||
return ConversationResponse.fromEntity(conv, lastMessage);
|
Long lastSenderId = last != null ? last.getSenderId() : null;
|
||||||
|
return ConversationResponse.fromEntity(conv, lastMessage, lastSenderId);
|
||||||
})
|
})
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
@@ -106,7 +110,8 @@ public class ChatService {
|
|||||||
Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1);
|
Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1);
|
||||||
String lastMessage = last != null && last.getContent() != null ? last.getContent() : "";
|
String lastMessage = last != null && last.getContent() != null ? last.getContent() : "";
|
||||||
|
|
||||||
return ConversationResponse.fromEntity(conversation, lastMessage);
|
Long lastSenderId = last != null ? last.getSenderId() : null;
|
||||||
|
return ConversationResponse.fromEntity(conversation, lastMessage, lastSenderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -147,7 +152,7 @@ public class ChatService {
|
|||||||
conversationRepository.save(conversation);
|
conversationRepository.save(conversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
return MessageResponse.fromEntity(message);
|
return toMessageResponse(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -190,7 +195,7 @@ public class ChatService {
|
|||||||
conversationRepository.save(conversation);
|
conversationRepository.save(conversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
return MessageResponse.fromEntity(message);
|
return toMessageResponse(message);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException("Failed to store attachment", e);
|
throw new RuntimeException("Failed to store attachment", e);
|
||||||
}
|
}
|
||||||
@@ -217,7 +222,8 @@ public class ChatService {
|
|||||||
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
||||||
Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1);
|
Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1);
|
||||||
String lastMessage = last != null && last.getContent() != null ? last.getContent() : "";
|
String lastMessage = last != null && last.getContent() != null ? last.getContent() : "";
|
||||||
return ConversationResponse.fromEntity(conversation, lastMessage);
|
Long lastSenderId = last != null ? last.getSenderId() : null;
|
||||||
|
return ConversationResponse.fromEntity(conversation, lastMessage, lastSenderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -240,7 +246,8 @@ public class ChatService {
|
|||||||
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
||||||
Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1);
|
Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1);
|
||||||
String lastMessage = last != null && last.getContent() != null ? last.getContent() : "";
|
String lastMessage = last != null && last.getContent() != null ? last.getContent() : "";
|
||||||
return ConversationResponse.fromEntity(conversation, lastMessage);
|
Long lastSenderId = last != null ? last.getSenderId() : null;
|
||||||
|
return ConversationResponse.fromEntity(conversation, lastMessage, lastSenderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MessageResponse> getMessages(Long conversationId, Long userId, User.Role role) {
|
public List<MessageResponse> getMessages(Long conversationId, Long userId, User.Role role) {
|
||||||
@@ -258,10 +265,20 @@ public class ChatService {
|
|||||||
|
|
||||||
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
||||||
return messages.stream()
|
return messages.stream()
|
||||||
.map(MessageResponse::fromEntity)
|
.map(this::toMessageResponse)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MessageResponse toMessageResponse(Message message) {
|
||||||
|
MessageResponse response = MessageResponse.fromEntity(message);
|
||||||
|
userRepository.findById(message.getSenderId()).ifPresent(user -> {
|
||||||
|
if (avatarStorageService.hasAvatar(user)) {
|
||||||
|
response.setSenderAvatarUrl(avatarStorageService.toOwnerAvatarUrl(user));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasConversationAccess(Long conversationId, Long userId, User.Role role) {
|
public boolean hasConversationAccess(Long conversationId, Long userId, User.Role role) {
|
||||||
Conversation conversation = conversationRepository.findById(conversationId)
|
Conversation conversation = conversationRepository.findById(conversationId)
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
.orElseThrow(() -> new ResourceNotFoundException("Conversation not found"));
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import java.net.http.WebSocket;
|
|||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionStage;
|
import java.util.concurrent.CompletionStage;
|
||||||
@@ -39,6 +41,10 @@ public class ChatRealtimeClient implements WebSocket.Listener {
|
|||||||
private Consumer<String> statusListener;
|
private Consumer<String> statusListener;
|
||||||
private volatile String currentStatus = "Chat disconnected";
|
private volatile String currentStatus = "Chat disconnected";
|
||||||
|
|
||||||
|
private final Map<Long, ConversationResponse> globalConversations = new HashMap<>();
|
||||||
|
private final List<Consumer<Boolean>> notificationListeners = new ArrayList<>();
|
||||||
|
private boolean lastNotificationState = false;
|
||||||
|
|
||||||
private ChatRealtimeClient() {
|
private ChatRealtimeClient() {
|
||||||
this.httpClient = HttpClient.newBuilder()
|
this.httpClient = HttpClient.newBuilder()
|
||||||
.connectTimeout(Duration.ofSeconds(10))
|
.connectTimeout(Duration.ofSeconds(10))
|
||||||
@@ -49,6 +55,59 @@ public class ChatRealtimeClient implements WebSocket.Listener {
|
|||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addNotificationListener(Consumer<Boolean> listener) {
|
||||||
|
synchronized (lock) {
|
||||||
|
notificationListeners.add(listener);
|
||||||
|
listener.accept(lastNotificationState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void initializeState(List<ConversationResponse> conversations) {
|
||||||
|
synchronized (lock) {
|
||||||
|
globalConversations.clear();
|
||||||
|
for (ConversationResponse conv : conversations) {
|
||||||
|
globalConversations.put(conv.getId(), conv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateNotificationState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasActionableChats() {
|
||||||
|
synchronized (lock) {
|
||||||
|
UserSession session = UserSession.getInstance();
|
||||||
|
Long currentUserId = session.getUserId();
|
||||||
|
for (ConversationResponse conv : globalConversations.values()) {
|
||||||
|
if ("CLOSED".equals(conv.getStatus())) continue;
|
||||||
|
|
||||||
|
// Needs pickup
|
||||||
|
if (conv.getHumanRequestedAt() != null && conv.getStaffId() == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Needs reply (assigned to me and last sender was someone else - customer)
|
||||||
|
if (currentUserId != null && currentUserId.equals(conv.getStaffId())) {
|
||||||
|
if (conv.getLastSenderId() != null && !conv.getLastSenderId().equals(currentUserId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateNotificationState() {
|
||||||
|
boolean currentState = hasActionableChats();
|
||||||
|
List<Consumer<Boolean>> listeners;
|
||||||
|
synchronized (lock) {
|
||||||
|
if (currentState == lastNotificationState) return;
|
||||||
|
lastNotificationState = currentState;
|
||||||
|
listeners = new ArrayList<>(notificationListeners);
|
||||||
|
}
|
||||||
|
for (Consumer<Boolean> listener : listeners) {
|
||||||
|
listener.accept(currentState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setConversationListener(Consumer<ConversationResponse> conversationListener) {
|
public void setConversationListener(Consumer<ConversationResponse> conversationListener) {
|
||||||
this.conversationListener = conversationListener;
|
this.conversationListener = conversationListener;
|
||||||
}
|
}
|
||||||
@@ -230,6 +289,8 @@ public class ChatRealtimeClient implements WebSocket.Listener {
|
|||||||
destinationBySubscription.clear();
|
destinationBySubscription.clear();
|
||||||
conversationsSubscriptionId = null;
|
conversationsSubscriptionId = null;
|
||||||
conversationMessagesSubscriptionId = null;
|
conversationMessagesSubscriptionId = null;
|
||||||
|
globalConversations.clear();
|
||||||
|
updateNotificationState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleFrame(String frame) {
|
private void handleFrame(String frame) {
|
||||||
@@ -272,11 +333,25 @@ public class ChatRealtimeClient implements WebSocket.Listener {
|
|||||||
if (messageListener != null) {
|
if (messageListener != null) {
|
||||||
messageListener.accept(message);
|
messageListener.accept(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also update globalConversation last sender if this is the active conversation
|
||||||
|
synchronized (lock) {
|
||||||
|
ConversationResponse conv = globalConversations.get(message.getConversationId());
|
||||||
|
if (conv != null) {
|
||||||
|
conv.setLastMessage(message.getContent());
|
||||||
|
conv.setLastSenderId(message.getSenderId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateNotificationState();
|
||||||
} else {
|
} else {
|
||||||
ConversationResponse conversation = ApiClient.getInstance().getObjectMapper().readValue(bodyPart, ConversationResponse.class);
|
ConversationResponse conversation = ApiClient.getInstance().getObjectMapper().readValue(bodyPart, ConversationResponse.class);
|
||||||
|
synchronized (lock) {
|
||||||
|
globalConversations.put(conversation.getId(), conversation);
|
||||||
|
}
|
||||||
if (conversationListener != null) {
|
if (conversationListener != null) {
|
||||||
conversationListener.accept(conversation);
|
conversationListener.accept(conversation);
|
||||||
}
|
}
|
||||||
|
updateNotificationState();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
publishStatus("Chat update failed");
|
publishStatus("Chat update failed");
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ public class ConversationResponse {
|
|||||||
private String status;
|
private String status;
|
||||||
private String mode;
|
private String mode;
|
||||||
private String lastMessage;
|
private String lastMessage;
|
||||||
|
private Long lastSenderId;
|
||||||
private LocalDateTime humanRequestedAt;
|
private LocalDateTime humanRequestedAt;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
@@ -16,6 +17,14 @@ public class ConversationResponse {
|
|||||||
public ConversationResponse() {
|
public ConversationResponse() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getLastSenderId() {
|
||||||
|
return lastSenderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastSenderId(Long lastSenderId) {
|
||||||
|
this.lastSenderId = lastSenderId;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ package org.example.petshopdesktop.api.dto.chat;
|
|||||||
|
|
||||||
public class MessageRequest {
|
public class MessageRequest {
|
||||||
private String content;
|
private String content;
|
||||||
private String attachmentUrl;
|
|
||||||
private String attachmentName;
|
|
||||||
private String attachmentMimeType;
|
|
||||||
private Long attachmentSizeBytes;
|
|
||||||
|
|
||||||
public MessageRequest() {
|
public MessageRequest() {
|
||||||
}
|
}
|
||||||
@@ -21,36 +17,4 @@ public class MessageRequest {
|
|||||||
public void setContent(String content) {
|
public void setContent(String content) {
|
||||||
this.content = content;
|
this.content = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAttachmentUrl() {
|
|
||||||
return attachmentUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAttachmentUrl(String attachmentUrl) {
|
|
||||||
this.attachmentUrl = attachmentUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAttachmentName() {
|
|
||||||
return attachmentName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAttachmentName(String attachmentName) {
|
|
||||||
this.attachmentName = attachmentName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAttachmentMimeType() {
|
|
||||||
return attachmentMimeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAttachmentMimeType(String attachmentMimeType) {
|
|
||||||
this.attachmentMimeType = attachmentMimeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getAttachmentSizeBytes() {
|
|
||||||
return attachmentSizeBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAttachmentSizeBytes(Long attachmentSizeBytes) {
|
|
||||||
this.attachmentSizeBytes = attachmentSizeBytes;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,10 @@ public class MessageResponse {
|
|||||||
private Long id;
|
private Long id;
|
||||||
private Long conversationId;
|
private Long conversationId;
|
||||||
private Long senderId;
|
private Long senderId;
|
||||||
|
private String senderAvatarUrl;
|
||||||
private String content;
|
private String content;
|
||||||
private LocalDateTime timestamp;
|
private LocalDateTime timestamp;
|
||||||
private Boolean isRead;
|
private Boolean isRead;
|
||||||
private String attachmentUrl;
|
|
||||||
private String attachmentName;
|
|
||||||
private String attachmentMimeType;
|
|
||||||
private Long attachmentSizeBytes;
|
|
||||||
|
|
||||||
public MessageResponse() {
|
public MessageResponse() {
|
||||||
}
|
}
|
||||||
@@ -41,6 +38,14 @@ public class MessageResponse {
|
|||||||
this.senderId = senderId;
|
this.senderId = senderId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getSenderAvatarUrl() {
|
||||||
|
return senderAvatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSenderAvatarUrl(String senderAvatarUrl) {
|
||||||
|
this.senderAvatarUrl = senderAvatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public String getContent() {
|
public String getContent() {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
@@ -64,36 +69,4 @@ public class MessageResponse {
|
|||||||
public void setIsRead(Boolean isRead) {
|
public void setIsRead(Boolean isRead) {
|
||||||
this.isRead = isRead;
|
this.isRead = isRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getAttachmentUrl() {
|
|
||||||
return attachmentUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAttachmentUrl(String attachmentUrl) {
|
|
||||||
this.attachmentUrl = attachmentUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAttachmentName() {
|
|
||||||
return attachmentName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAttachmentName(String attachmentName) {
|
|
||||||
this.attachmentName = attachmentName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAttachmentMimeType() {
|
|
||||||
return attachmentMimeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAttachmentMimeType(String attachmentMimeType) {
|
|
||||||
this.attachmentMimeType = attachmentMimeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getAttachmentSizeBytes() {
|
|
||||||
return attachmentSizeBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAttachmentSizeBytes(Long attachmentSizeBytes) {
|
|
||||||
this.attachmentSizeBytes = attachmentSizeBytes;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import org.example.petshopdesktop.api.dto.chat.ConversationRequest;
|
|||||||
import org.example.petshopdesktop.api.dto.chat.ConversationResponse;
|
import org.example.petshopdesktop.api.dto.chat.ConversationResponse;
|
||||||
import org.example.petshopdesktop.api.dto.chat.MessageRequest;
|
import org.example.petshopdesktop.api.dto.chat.MessageRequest;
|
||||||
import org.example.petshopdesktop.api.dto.chat.MessageResponse;
|
import org.example.petshopdesktop.api.dto.chat.MessageResponse;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ChatApi {
|
public class ChatApi {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package org.example.petshopdesktop.controllers;
|
|||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
@@ -15,6 +16,9 @@ import javafx.scene.layout.HBox;
|
|||||||
import javafx.scene.layout.Priority;
|
import javafx.scene.layout.Priority;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
import javafx.scene.paint.ImagePattern;
|
||||||
|
import javafx.scene.shape.Circle;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
import org.example.petshopdesktop.api.ChatRealtimeClient;
|
import org.example.petshopdesktop.api.ChatRealtimeClient;
|
||||||
import org.example.petshopdesktop.api.dto.chat.ConversationResponse;
|
import org.example.petshopdesktop.api.dto.chat.ConversationResponse;
|
||||||
import org.example.petshopdesktop.api.dto.chat.MessageRequest;
|
import org.example.petshopdesktop.api.dto.chat.MessageRequest;
|
||||||
@@ -25,6 +29,7 @@ import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
|||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -53,6 +58,9 @@ public class ChatController {
|
|||||||
@FXML
|
@FXML
|
||||||
private Button btnRefresh;
|
private Button btnRefresh;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button btnAttachment;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Label lblConversationTitle;
|
private Label lblConversationTitle;
|
||||||
|
|
||||||
@@ -63,6 +71,7 @@ public class ChatController {
|
|||||||
private final Map<Long, String> customerLabels = new HashMap<>();
|
private final Map<Long, String> customerLabels = new HashMap<>();
|
||||||
private final ChatRealtimeClient realtimeClient = ChatRealtimeClient.getInstance();
|
private final ChatRealtimeClient realtimeClient = ChatRealtimeClient.getInstance();
|
||||||
private ConversationResponse selectedConversation;
|
private ConversationResponse selectedConversation;
|
||||||
|
private File selectedAttachmentFile;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
@@ -78,7 +87,19 @@ public class ChatController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Label title = new Label(getConversationTitle(item));
|
Label title = new Label(getConversationTitle(item));
|
||||||
title.setStyle("-fx-font-weight: bold; -fx-text-fill: #1f2937;");
|
|
||||||
|
// Bold title if needs attention
|
||||||
|
UserSession session = UserSession.getInstance();
|
||||||
|
Long currentUserId = session.getUserId();
|
||||||
|
boolean needsPickup = item.getHumanRequestedAt() != null && item.getStaffId() == null;
|
||||||
|
boolean needsReply = currentUserId != null && currentUserId.equals(item.getStaffId())
|
||||||
|
&& item.getLastSenderId() != null && !item.getLastSenderId().equals(currentUserId);
|
||||||
|
|
||||||
|
if (needsPickup || needsReply) {
|
||||||
|
title.setStyle("-fx-font-weight: bold; -fx-text-fill: #FF6B6B;");
|
||||||
|
} else {
|
||||||
|
title.setStyle("-fx-font-weight: bold; -fx-text-fill: #1f2937;");
|
||||||
|
}
|
||||||
Label preview = new Label(item.getLastMessage() == null ? "" : item.getLastMessage());
|
Label preview = new Label(item.getLastMessage() == null ? "" : item.getLastMessage());
|
||||||
preview.setStyle("-fx-text-fill: #64748b;");
|
preview.setStyle("-fx-text-fill: #64748b;");
|
||||||
preview.setWrapText(true);
|
preview.setWrapText(true);
|
||||||
@@ -135,13 +156,62 @@ public class ChatController {
|
|||||||
|
|
||||||
String content = txtMessage.getText() == null ? "" : txtMessage.getText().trim();
|
String content = txtMessage.getText() == null ? "" : txtMessage.getText().trim();
|
||||||
if (content.isEmpty()) {
|
if (content.isEmpty()) {
|
||||||
|
if (selectedAttachmentFile != null) {
|
||||||
|
lblChatStatus.setText("Attachments are not available yet");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Long convId = selectedConversation.getId();
|
||||||
|
|
||||||
txtMessage.clear();
|
txtMessage.clear();
|
||||||
btnSend.setDisable(true);
|
btnSend.setDisable(true);
|
||||||
|
|
||||||
lblChatStatus.setText("Sending message...");
|
lblChatStatus.setText("Sending message...");
|
||||||
sendMessage(selectedConversation.getId(), content);
|
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
MessageRequest request = new MessageRequest(content);
|
||||||
|
MessageResponse response = ChatApi.getInstance().sendMessage(convId, request);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
btnSend.setDisable(false);
|
||||||
|
appendMessageIfSelected(response);
|
||||||
|
if (selectedAttachmentFile != null) {
|
||||||
|
clearLocalAttachment();
|
||||||
|
lblChatStatus.setText("Message sent without attachment");
|
||||||
|
} else {
|
||||||
|
lblChatStatus.setText("Message sent");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
txtMessage.setText(content);
|
||||||
|
btnSend.setDisable(false);
|
||||||
|
lblChatStatus.setText("Chat send failed");
|
||||||
|
ActivityLogger.getInstance().logException(
|
||||||
|
"ChatController.sendMessage",
|
||||||
|
e,
|
||||||
|
"Sending chat message for conversation " + convId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnAttachmentClicked(ActionEvent event) {
|
||||||
|
File file = org.example.petshopdesktop.util.FilePickerSupport.pickAnyFile(btnAttachment.getScene().getWindow());
|
||||||
|
if (file == null) return;
|
||||||
|
|
||||||
|
selectedAttachmentFile = file;
|
||||||
|
btnAttachment.setText("📎 " + file.getName());
|
||||||
|
btnAttachment.setStyle("-fx-background-color: #dcfce7; -fx-background-radius: 12; -fx-text-fill: #166534; -fx-cursor: hand;");
|
||||||
|
lblChatStatus.setText("Attachment selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearLocalAttachment() {
|
||||||
|
selectedAttachmentFile = null;
|
||||||
|
btnAttachment.setText("📎");
|
||||||
|
btnAttachment.setStyle("-fx-background-color: #e2e8f0; -fx-background-radius: 12; -fx-text-fill: #475569; -fx-cursor: hand;");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadCustomers() {
|
private void loadCustomers() {
|
||||||
@@ -207,31 +277,6 @@ public class ChatController {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendMessage(Long conversationId, String content) {
|
|
||||||
new Thread(() -> {
|
|
||||||
try {
|
|
||||||
MessageResponse response = ChatApi.getInstance().sendMessage(conversationId, new MessageRequest(content));
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
btnSend.setDisable(false);
|
|
||||||
appendMessageIfSelected(response);
|
|
||||||
if (selectedConversation != null && selectedConversation.getId().equals(conversationId)) {
|
|
||||||
lblChatStatus.setText("Message sent");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
txtMessage.setText(content);
|
|
||||||
btnSend.setDisable(false);
|
|
||||||
lblChatStatus.setText("Chat send failed");
|
|
||||||
ActivityLogger.getInstance().logException(
|
|
||||||
"ChatController.sendMessage",
|
|
||||||
e,
|
|
||||||
"Sending chat message for conversation " + conversationId);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void renderMessages(List<MessageResponse> messages) {
|
private void renderMessages(List<MessageResponse> messages) {
|
||||||
vbMessages.getChildren().clear();
|
vbMessages.getChildren().clear();
|
||||||
for (MessageResponse message : messages) {
|
for (MessageResponse message : messages) {
|
||||||
@@ -307,6 +352,20 @@ public class ChatController {
|
|||||||
|
|
||||||
private HBox createMessageBubble(MessageResponse message) {
|
private HBox createMessageBubble(MessageResponse message) {
|
||||||
boolean mine = message.getSenderId() != null && message.getSenderId().equals(UserSession.getInstance().getUserId());
|
boolean mine = message.getSenderId() != null && message.getSenderId().equals(UserSession.getInstance().getUserId());
|
||||||
|
|
||||||
|
Circle avatar = new Circle(16);
|
||||||
|
avatar.setFill(javafx.scene.paint.Color.web(mine ? "#0f766e" : "#cbd5e1"));
|
||||||
|
if (message.getSenderAvatarUrl() != null && !message.getSenderAvatarUrl().isBlank()) {
|
||||||
|
try {
|
||||||
|
String fullUrl = org.example.petshopdesktop.api.ApiConfig.getInstance().getBaseUrl() + message.getSenderAvatarUrl();
|
||||||
|
Image img = new Image(fullUrl, true);
|
||||||
|
img.errorProperty().addListener((obs, old, err) -> {
|
||||||
|
if (err) avatar.setFill(javafx.scene.paint.Color.web(mine ? "#0f766e" : "#cbd5e1"));
|
||||||
|
});
|
||||||
|
avatar.setFill(new ImagePattern(img));
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
|
||||||
Label author = new Label(resolveAuthorLabel(message));
|
Label author = new Label(resolveAuthorLabel(message));
|
||||||
author.setStyle("-fx-font-weight: bold; -fx-text-fill: " + (mine ? "#ffffff" : "#1f2937") + ";");
|
author.setStyle("-fx-font-weight: bold; -fx-text-fill: " + (mine ? "#ffffff" : "#1f2937") + ";");
|
||||||
|
|
||||||
@@ -323,20 +382,6 @@ public class ChatController {
|
|||||||
bubble.getChildren().add(content);
|
bubble.getChildren().add(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.getAttachmentUrl() != null && !message.getAttachmentUrl().isBlank()) {
|
|
||||||
String attachmentLabel = message.getAttachmentName();
|
|
||||||
if (attachmentLabel == null || attachmentLabel.isBlank()) {
|
|
||||||
attachmentLabel = "Attachment";
|
|
||||||
}
|
|
||||||
if (message.getAttachmentSizeBytes() != null && message.getAttachmentSizeBytes() > 0) {
|
|
||||||
attachmentLabel = attachmentLabel + " (" + formatSize(message.getAttachmentSizeBytes()) + ")";
|
|
||||||
}
|
|
||||||
Label attachment = new Label(attachmentLabel);
|
|
||||||
attachment.setWrapText(true);
|
|
||||||
attachment.setStyle("-fx-text-fill: " + (mine ? "#dbeafe" : "#0f766e") + "; -fx-underline: true;");
|
|
||||||
bubble.getChildren().add(attachment);
|
|
||||||
}
|
|
||||||
|
|
||||||
bubble.getChildren().add(timestamp);
|
bubble.getChildren().add(timestamp);
|
||||||
bubble.setMaxWidth(420);
|
bubble.setMaxWidth(420);
|
||||||
bubble.setStyle(mine
|
bubble.setStyle(mine
|
||||||
@@ -346,10 +391,13 @@ public class ChatController {
|
|||||||
Region spacer = new Region();
|
Region spacer = new Region();
|
||||||
HBox.setHgrow(spacer, Priority.ALWAYS);
|
HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||||
HBox container = new HBox(12);
|
HBox container = new HBox(12);
|
||||||
|
container.setAlignment(javafx.geometry.Pos.BOTTOM_LEFT);
|
||||||
|
|
||||||
if (mine) {
|
if (mine) {
|
||||||
container.getChildren().addAll(spacer, bubble);
|
container.getChildren().addAll(spacer, bubble, avatar);
|
||||||
|
container.setAlignment(javafx.geometry.Pos.BOTTOM_RIGHT);
|
||||||
} else {
|
} else {
|
||||||
container.getChildren().addAll(bubble, spacer);
|
container.getChildren().addAll(avatar, bubble, spacer);
|
||||||
}
|
}
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
@@ -392,17 +440,4 @@ public class ChatController {
|
|||||||
private void scrollMessagesToBottom() {
|
private void scrollMessagesToBottom() {
|
||||||
Platform.runLater(() -> spMessages.setVvalue(1.0));
|
Platform.runLater(() -> spMessages.setVvalue(1.0));
|
||||||
}
|
}
|
||||||
private String formatSize(Long bytes) {
|
|
||||||
if (bytes == null || bytes <= 0) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
double size = bytes;
|
|
||||||
String[] units = {"B", "KB", "MB", "GB"};
|
|
||||||
int unitIndex = 0;
|
|
||||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
||||||
size = size / 1024;
|
|
||||||
unitIndex++;
|
|
||||||
}
|
|
||||||
return unitIndex == 0 ? String.format("%.0f %s", size, units[unitIndex]) : String.format("%.1f %s", size, units[unitIndex]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import javafx.scene.paint.Color;
|
|||||||
import javafx.scene.paint.ImagePattern;
|
import javafx.scene.paint.ImagePattern;
|
||||||
import javafx.scene.shape.Circle;
|
import javafx.scene.shape.Circle;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import org.example.petshopdesktop.api.endpoints.ChatApi;
|
||||||
import org.example.petshopdesktop.api.ChatRealtimeClient;
|
import org.example.petshopdesktop.api.ChatRealtimeClient;
|
||||||
import org.example.petshopdesktop.api.dto.auth.AvatarUploadResponse;
|
import org.example.petshopdesktop.api.dto.auth.AvatarUploadResponse;
|
||||||
import org.example.petshopdesktop.api.dto.auth.UserInfoResponse;
|
import org.example.petshopdesktop.api.dto.auth.UserInfoResponse;
|
||||||
@@ -55,7 +56,10 @@ public class MainLayoutController {
|
|||||||
private Button btnInventory;
|
private Button btnInventory;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Button btnLogout;
|
private Button btnChat;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Circle chatBadge;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Button btnPets;
|
private Button btnPets;
|
||||||
@@ -85,7 +89,7 @@ public class MainLayoutController {
|
|||||||
private Button btnAnalytics;
|
private Button btnAnalytics;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Button btnChat;
|
private Button btnLogout;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private StackPane logoContainer;
|
private StackPane logoContainer;
|
||||||
@@ -263,6 +267,14 @@ public class MainLayoutController {
|
|||||||
refreshProfileHeader();
|
refreshProfileHeader();
|
||||||
applyRBAC();
|
applyRBAC();
|
||||||
|
|
||||||
|
ChatRealtimeClient.getInstance().addNotificationListener(hasActionable -> {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (chatBadge != null) {
|
||||||
|
chatBadge.setVisible(hasActionable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
UserSession session = UserSession.getInstance();
|
UserSession session = UserSession.getInstance();
|
||||||
if (session.isAdmin()) {
|
if (session.isAdmin()) {
|
||||||
loadView("analytics-view.fxml");
|
loadView("analytics-view.fxml");
|
||||||
@@ -391,7 +403,16 @@ public class MainLayoutController {
|
|||||||
|
|
||||||
btnSalesHistory.setText(isAdmin ? "Sales History" : "Sales");
|
btnSalesHistory.setText(isAdmin ? "Sales History" : "Sales");
|
||||||
|
|
||||||
|
// Initial chat state and subscription
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
var conversations = ChatApi.getInstance().listConversations();
|
||||||
|
ChatRealtimeClient.getInstance().initializeState(conversations);
|
||||||
|
ChatRealtimeClient.getInstance().subscribeToConversations();
|
||||||
|
} catch (Exception e) {
|
||||||
|
ActivityLogger.getInstance().logException("MainLayoutController.applyRBAC", e, "Initializing chat notifications");
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadView(String fxmlFile) {
|
private void loadView(String fxmlFile) {
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ public final class FilePickerSupport {
|
|||||||
return pickImageFileWithJavaFx(ownerWindow);
|
return pickImageFileWithJavaFx(ownerWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static File pickAnyFile(Window ownerWindow) {
|
||||||
|
if (shouldUseAwtPicker()) {
|
||||||
|
return pickAnyFileWithSwing();
|
||||||
|
}
|
||||||
|
return pickAnyFileWithJavaFx(ownerWindow);
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean shouldUseAwtPicker() {
|
private static boolean shouldUseAwtPicker() {
|
||||||
if (GraphicsEnvironment.isHeadless()) {
|
if (GraphicsEnvironment.isHeadless()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -40,6 +47,12 @@ public final class FilePickerSupport {
|
|||||||
return chooser.showOpenDialog(ownerWindow);
|
return chooser.showOpenDialog(ownerWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static File pickAnyFileWithJavaFx(Window ownerWindow) {
|
||||||
|
FileChooser chooser = new FileChooser();
|
||||||
|
chooser.setTitle("Choose File Attachment");
|
||||||
|
return chooser.showOpenDialog(ownerWindow);
|
||||||
|
}
|
||||||
|
|
||||||
private static File pickImageFileWithSwing() {
|
private static File pickImageFileWithSwing() {
|
||||||
AtomicReference<File> selectedFile = new AtomicReference<>();
|
AtomicReference<File> selectedFile = new AtomicReference<>();
|
||||||
Runnable dialogTask = () -> {
|
Runnable dialogTask = () -> {
|
||||||
@@ -74,4 +87,38 @@ public final class FilePickerSupport {
|
|||||||
|
|
||||||
return selectedFile.get();
|
return selectedFile.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static File pickAnyFileWithSwing() {
|
||||||
|
AtomicReference<File> selectedFile = new AtomicReference<>();
|
||||||
|
Runnable dialogTask = () -> {
|
||||||
|
try {
|
||||||
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
JFileChooser chooser = new JFileChooser();
|
||||||
|
chooser.setDialogTitle("Choose File Attachment");
|
||||||
|
chooser.setAcceptAllFileFilterUsed(true);
|
||||||
|
|
||||||
|
int result = chooser.showOpenDialog((Component) null);
|
||||||
|
if (result == JFileChooser.APPROVE_OPTION) {
|
||||||
|
selectedFile.set(chooser.getSelectedFile());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (java.awt.EventQueue.isDispatchThread()) {
|
||||||
|
dialogTask.run();
|
||||||
|
} else {
|
||||||
|
java.awt.EventQueue.invokeAndWait(dialogTask);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return null;
|
||||||
|
} catch (InvocationTargetException ex) {
|
||||||
|
throw new IllegalStateException("Failed to open Swing file picker", ex.getCause());
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedFile.get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
<?import javafx.geometry.Insets?>
|
<?import javafx.geometry.Insets?>
|
||||||
<?import javafx.scene.effect.DropShadow?>
|
<?import javafx.scene.effect.DropShadow?>
|
||||||
|
<?import javafx.scene.layout.StackPane?>
|
||||||
|
<?import javafx.scene.shape.Circle?>
|
||||||
<?import javafx.scene.control.Button?>
|
<?import javafx.scene.control.Button?>
|
||||||
<?import javafx.scene.control.Label?>
|
<?import javafx.scene.control.Label?>
|
||||||
<?import javafx.scene.control.ScrollPane?>
|
<?import javafx.scene.control.ScrollPane?>
|
||||||
@@ -123,14 +125,21 @@
|
|||||||
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
|
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
|
||||||
</padding>
|
</padding>
|
||||||
</Button>
|
</Button>
|
||||||
<Button fx:id="btnChat" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnChatClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Chat" textFill="#cbd5e1">
|
<StackPane alignment="CENTER_LEFT">
|
||||||
<font>
|
<Button fx:id="btnChat" alignment="CENTER_LEFT" maxWidth="Infinity" mnemonicParsing="false" onAction="#btnChatClicked" style="-fx-background-color: transparent; -fx-background-radius: 8; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;" text="Chat" textFill="#cbd5e1">
|
||||||
<Font name="System" size="12.0" />
|
<font>
|
||||||
</font>
|
<Font name="System" size="12.0" />
|
||||||
<padding>
|
</font>
|
||||||
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
|
<padding>
|
||||||
</padding>
|
<Insets bottom="8.0" left="10.0" right="10.0" top="8.0" />
|
||||||
</Button>
|
</padding>
|
||||||
|
</Button>
|
||||||
|
<javafx.scene.shape.Circle fx:id="chatBadge" fill="#ff4d4d" radius="4.0" visible="false" StackPane.alignment="CENTER_LEFT">
|
||||||
|
<StackPane.margin>
|
||||||
|
<Insets left="40.0" top="-8.0" />
|
||||||
|
</StackPane.margin>
|
||||||
|
</javafx.scene.shape.Circle>
|
||||||
|
</StackPane>
|
||||||
|
|
||||||
<Separator prefWidth="200.0" style="-fx-background-color: #444444; -fx-opacity: 0.35;" />
|
<Separator prefWidth="200.0" style="-fx-background-color: #444444; -fx-opacity: 0.35;" />
|
||||||
|
|
||||||
|
|||||||
@@ -61,8 +61,16 @@
|
|||||||
<VBox spacing="10.0" style="-fx-background-color: #ffffff; -fx-background-radius: 16; -fx-padding: 14;">
|
<VBox spacing="10.0" style="-fx-background-color: #ffffff; -fx-background-radius: 16; -fx-padding: 14;">
|
||||||
<children>
|
<children>
|
||||||
<TextArea fx:id="txtMessage" prefRowCount="4" promptText="Reply to the selected conversation..." wrapText="true" />
|
<TextArea fx:id="txtMessage" prefRowCount="4" promptText="Reply to the selected conversation..." wrapText="true" />
|
||||||
<HBox alignment="CENTER_RIGHT">
|
<HBox alignment="CENTER_RIGHT" spacing="10.0">
|
||||||
<children>
|
<children>
|
||||||
|
<Button fx:id="btnAttachment" mnemonicParsing="false" onAction="#btnAttachmentClicked" style="-fx-background-color: #e2e8f0; -fx-background-radius: 12; -fx-text-fill: #475569; -fx-cursor: hand;" text="📎">
|
||||||
|
<padding>
|
||||||
|
<Insets bottom="10.0" left="14.0" right="14.0" top="10.0" />
|
||||||
|
</padding>
|
||||||
|
<font>
|
||||||
|
<Font size="14.0" />
|
||||||
|
</font>
|
||||||
|
</Button>
|
||||||
<Button fx:id="btnSend" mnemonicParsing="false" onAction="#btnSendClicked" style="-fx-background-color: #1d4ed8; -fx-background-radius: 12; -fx-text-fill: white; -fx-cursor: hand;" text="Send Message">
|
<Button fx:id="btnSend" mnemonicParsing="false" onAction="#btnSendClicked" style="-fx-background-color: #1d4ed8; -fx-background-radius: 12; -fx-text-fill: white; -fx-cursor: hand;" text="Send Message">
|
||||||
<padding>
|
<padding>
|
||||||
<Insets bottom="10.0" left="18.0" right="18.0" top="10.0" />
|
<Insets bottom="10.0" left="18.0" right="18.0" top="10.0" />
|
||||||
|
|||||||
Reference in New Issue
Block a user