feat:【站内信】

This commit is contained in:
zpaeng
2025-08-21 23:42:53 +08:00
parent d8b3c68150
commit 84ab87878a
27 changed files with 1970 additions and 14 deletions

View File

@@ -92,7 +92,7 @@ public class SecurityConfig {
cfg.setAllowedHeaders(List.of("*"));
cfg.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", cfg);
source.registerCorsConfiguration("/**", cfg);
return source;
}
@@ -104,6 +104,7 @@ public class SecurityConfig {
.exceptionHandling(eh -> eh.accessDeniedHandler(customAccessDeniedHandler))
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.requestMatchers("/ws/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/comments/**").permitAll()
@@ -172,7 +173,7 @@ public class SecurityConfig {
response.getWriter().write("{\"error\": \"Invalid or expired token\"}");
return;
}
} else if (!uri.startsWith("/api/auth") && !publicGet) {
} else if (!uri.startsWith("/api/auth") && !publicGet && !uri.startsWith("/ws")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"error\": \"Missing token\"}");

View File

@@ -0,0 +1,79 @@
package com.openisle.config;
import com.openisle.service.JwtService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// Enable a simple memory-based message broker to carry the messages back to the client on destinations prefixed with "/topic" and "/queue"
config.enableSimpleBroker("/topic", "/queue");
// Set user destination prefix for personal messages
config.setUserDestinationPrefix("/user");
// Designates the "/app" prefix for messages that are bound for @MessageMapping-annotated methods.
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// Registers the "/ws" endpoint, enabling SockJS fallback options so that alternate transports may be used if WebSocket is not available.
registry.addEndpoint("/ws").setAllowedOriginPatterns("*").withSockJS();
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ChannelInterceptor() {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
System.out.println("WebSocket CONNECT command received");
String authHeader = accessor.getFirstNativeHeader("Authorization");
System.out.println("Authorization header: " + (authHeader != null ? "present" : "missing"));
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
try {
String username = jwtService.validateAndGetSubject(token);
System.out.println("JWT validated for user: " + username);
var userDetails = userDetailsService.loadUserByUsername(username);
Authentication auth = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
accessor.setUser(auth);
System.out.println("WebSocket user set: " + username);
} catch (Exception e) {
System.err.println("JWT validation failed: " + e.getMessage());
}
}
} else if (StompCommand.SUBSCRIBE.equals(accessor.getCommand())) {
System.out.println("WebSocket SUBSCRIBE to: " + accessor.getDestination());
System.out.println("WebSocket user during subscribe: " + (accessor.getUser() != null ? accessor.getUser().getName() : "null"));
}
return message;
}
});
}
}

View File

@@ -0,0 +1,117 @@
package com.openisle.controller;
import com.openisle.dto.ConversationDetailDto;
import com.openisle.dto.ConversationDto;
import com.openisle.dto.CreateConversationRequest;
import com.openisle.dto.CreateConversationResponse;
import com.openisle.dto.MessageDto;
import com.openisle.dto.UserSummaryDto;
import com.openisle.model.Message;
import com.openisle.model.MessageConversation;
import com.openisle.model.User;
import com.openisle.repository.UserRepository;
import com.openisle.service.MessageService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/messages")
@RequiredArgsConstructor
public class MessageController {
private final MessageService messageService;
private final UserRepository userRepository;
// This is a placeholder for getting the current user's ID
private Long getCurrentUserId(Authentication auth) {
User user = userRepository.findByUsername(auth.getName()).orElseThrow(() -> new IllegalArgumentException("Sender not found"));
// In a real application, you would get this from the Authentication object
return user.getId();
}
@GetMapping("/conversations")
public ResponseEntity<List<ConversationDto>> getConversations(Authentication auth) {
List<ConversationDto> conversations = messageService.getConversations(getCurrentUserId(auth));
return ResponseEntity.ok(conversations);
}
@GetMapping("/conversations/{conversationId}")
public ResponseEntity<ConversationDetailDto> getMessages(@PathVariable Long conversationId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
Authentication auth) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
ConversationDetailDto conversationDetails = messageService.getConversationDetails(conversationId, getCurrentUserId(auth), pageable);
return ResponseEntity.ok(conversationDetails);
}
@PostMapping
public ResponseEntity<MessageDto> sendMessage(@RequestBody MessageRequest req, Authentication auth) {
Message message = messageService.sendMessage(getCurrentUserId(auth), req.getRecipientId(), req.getContent());
return ResponseEntity.ok(toDto(message));
}
@PostMapping("/conversations/{conversationId}/read")
public ResponseEntity<Void> markAsRead(@PathVariable Long conversationId, Authentication auth) {
messageService.markConversationAsRead(conversationId, getCurrentUserId(auth));
return ResponseEntity.ok().build();
}
@PostMapping("/conversations")
public ResponseEntity<CreateConversationResponse> findOrCreateConversation(@RequestBody CreateConversationRequest req, Authentication auth) {
MessageConversation conversation = messageService.findOrCreateConversation(getCurrentUserId(auth), req.getRecipientId());
return ResponseEntity.ok(new CreateConversationResponse(conversation.getId()));
}
private MessageDto toDto(Message message) {
MessageDto dto = new MessageDto();
dto.setId(message.getId());
dto.setContent(message.getContent());
dto.setCreatedAt(message.getCreatedAt());
dto.setConversationId(message.getConversation().getId());
UserSummaryDto senderDto = new UserSummaryDto();
senderDto.setId(message.getSender().getId());
senderDto.setUsername(message.getSender().getUsername());
senderDto.setAvatar(message.getSender().getAvatar());
dto.setSender(senderDto);
return dto;
}
@GetMapping("/unread-count")
public ResponseEntity<Long> getUnreadCount(Authentication auth) {
return ResponseEntity.ok(messageService.getUnreadMessageCount(getCurrentUserId(auth)));
}
// A simple request DTO
static class MessageRequest {
private Long recipientId;
private String content;
public Long getRecipientId() {
return recipientId;
}
public void setRecipientId(Long recipientId) {
this.recipientId = recipientId;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
}
}

View File

@@ -0,0 +1,13 @@
package com.openisle.dto;
import lombok.Data;
import org.springframework.data.domain.Page;
import java.util.List;
@Data
public class ConversationDetailDto {
private Long id;
private List<UserSummaryDto> participants;
private Page<MessageDto> messages;
}

View File

@@ -0,0 +1,17 @@
package com.openisle.dto;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.List;
@Getter
@Setter
public class ConversationDto {
private Long id;
private MessageDto lastMessage;
private List<UserSummaryDto> participants;
private LocalDateTime createdAt;
private long unreadCount;
}

View File

@@ -0,0 +1,8 @@
package com.openisle.dto;
import lombok.Data;
@Data
public class CreateConversationRequest {
private Long recipientId;
}

View File

@@ -0,0 +1,12 @@
package com.openisle.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CreateConversationResponse {
private Long conversationId;
}

View File

@@ -0,0 +1,13 @@
package com.openisle.dto;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class MessageDto {
private Long id;
private String content;
private UserSummaryDto sender;
private Long conversationId;
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,10 @@
package com.openisle.dto;
import lombok.Data;
@Data
public class UserSummaryDto {
private Long id;
private String username;
private String avatar;
}

View File

@@ -0,0 +1,35 @@
package com.openisle.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "messages")
public class Message {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "conversation_id")
private MessageConversation conversation;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "sender_id")
private User sender;
@Column(nullable = false, columnDefinition = "TEXT")
private String content;
@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
}

View File

@@ -0,0 +1,36 @@
package com.openisle.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "message_conversations")
public class MessageConversation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "last_message_id")
private Message lastMessage;
@OneToMany(mappedBy = "conversation", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<MessageParticipant> participants = new HashSet<>();
@OneToMany(mappedBy = "conversation", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Message> messages = new HashSet<>();
}

View File

@@ -0,0 +1,30 @@
package com.openisle.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Entity
@Getter
@Setter
@NoArgsConstructor
@Table(name = "message_participants")
public class MessageParticipant {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "conversation_id")
private MessageConversation conversation;
@ManyToOne(optional = false, fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@Column
private LocalDateTime lastReadAt;
}

View File

@@ -0,0 +1,31 @@
package com.openisle.repository;
import com.openisle.model.MessageConversation;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.openisle.model.User;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
import com.openisle.model.User;
import java.util.List;
@Repository
public interface MessageConversationRepository extends JpaRepository<MessageConversation, Long> {
@Query("SELECT c FROM MessageConversation c JOIN c.participants p1 JOIN c.participants p2 WHERE p1.user = :user1 AND p2.user = :user2")
Optional<MessageConversation> findConversationByUsers(@Param("user1") User user1, @Param("user2") User user2);
@Query("SELECT DISTINCT c FROM MessageConversation c " +
"JOIN c.participants p " +
"LEFT JOIN FETCH c.lastMessage lm " +
"LEFT JOIN FETCH lm.sender " +
"LEFT JOIN FETCH c.participants cp " +
"LEFT JOIN FETCH cp.user " +
"WHERE p.user.id = :userId " +
"ORDER BY COALESCE(lm.createdAt, c.createdAt) DESC")
List<MessageConversation> findConversationsByUserIdOrderByLastMessageDesc(@Param("userId") Long userId);
}

View File

@@ -0,0 +1,14 @@
package com.openisle.repository;
import com.openisle.model.MessageParticipant;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface MessageParticipantRepository extends JpaRepository<MessageParticipant, Long> {
Optional<MessageParticipant> findByConversationIdAndUserId(Long conversationId, Long userId);
List<MessageParticipant> findByUserId(Long userId);
}

View File

@@ -0,0 +1,21 @@
package com.openisle.repository;
import com.openisle.model.Message;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface MessageRepository extends JpaRepository<Message, Long> {
List<Message> findByConversationIdOrderByCreatedAtAsc(Long conversationId);
Page<Message> findByConversationId(Long conversationId, Pageable pageable);
long countByConversationIdAndCreatedAtAfter(Long conversationId, java.time.LocalDateTime createdAt);
// 只计算不是指定用户发送的消息(即别人发给当前用户的消息)
long countByConversationIdAndCreatedAtAfterAndSenderIdNot(Long conversationId, java.time.LocalDateTime createdAt, Long senderId);
}

View File

@@ -0,0 +1,217 @@
package com.openisle.service;
import com.openisle.model.Message;
import com.openisle.model.MessageConversation;
import com.openisle.model.MessageParticipant;
import com.openisle.model.User;
import com.openisle.repository.MessageConversationRepository;
import com.openisle.repository.MessageParticipantRepository;
import com.openisle.repository.MessageRepository;
import com.openisle.repository.UserRepository;
import com.openisle.dto.ConversationDetailDto;
import com.openisle.dto.ConversationDto;
import com.openisle.dto.MessageDto;
import com.openisle.dto.UserSummaryDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class MessageService {
private final MessageRepository messageRepository;
private final MessageConversationRepository conversationRepository;
private final MessageParticipantRepository participantRepository;
private final UserRepository userRepository;
private final SimpMessagingTemplate messagingTemplate;
@Transactional
public Message sendMessage(Long senderId, Long recipientId, String content) {
log.info("Attempting to send message from user {} to user {}", senderId, recipientId);
User sender = userRepository.findById(senderId)
.orElseThrow(() -> new IllegalArgumentException("Sender not found"));
User recipient = userRepository.findById(recipientId)
.orElseThrow(() -> new IllegalArgumentException("Recipient not found"));
log.info("Finding or creating conversation for users {} and {}", sender.getUsername(), recipient.getUsername());
MessageConversation conversation = findOrCreateConversation(sender, recipient);
log.info("Conversation found or created with ID: {}", conversation.getId());
Message message = new Message();
message.setConversation(conversation);
message.setSender(sender);
message.setContent(content);
message = messageRepository.save(message);
log.info("Message saved with ID: {}", message.getId());
conversation.setLastMessage(message);
conversationRepository.save(conversation);
log.info("Conversation {} updated with last message ID {}", conversation.getId(), message.getId());
// Broadcast the new message to subscribed clients
MessageDto messageDto = toDto(message);
String conversationDestination = "/topic/conversation/" + conversation.getId();
messagingTemplate.convertAndSend(conversationDestination, messageDto);
log.info("Message {} broadcasted to destination: {}", message.getId(), conversationDestination);
// Also notify the recipient on their personal channel to update the conversation list
String userDestination = "/topic/user/" + recipient.getId() + "/messages";
messagingTemplate.convertAndSend(userDestination, messageDto);
log.info("Message {} notification sent to destination: {}", message.getId(), userDestination);
// Notify recipient of new unread count
long unreadCount = getUnreadMessageCount(recipientId);
log.info("Calculating unread count for user {}: {}", recipientId, unreadCount);
// Send using username instead of user ID for WebSocket routing
String recipientUsername = recipient.getUsername();
messagingTemplate.convertAndSendToUser(recipientUsername, "/queue/unread-count", unreadCount);
log.info("Sent unread count {} to user {} (username: {}) via WebSocket destination: /user/{}/queue/unread-count",
unreadCount, recipientId, recipientUsername, recipientUsername);
return message;
}
private MessageDto toDto(Message message) {
MessageDto dto = new MessageDto();
dto.setId(message.getId());
dto.setContent(message.getContent());
dto.setConversationId(message.getConversation().getId());
dto.setCreatedAt(message.getCreatedAt());
UserSummaryDto userSummaryDto = new UserSummaryDto();
userSummaryDto.setId(message.getSender().getId());
userSummaryDto.setUsername(message.getSender().getUsername());
userSummaryDto.setAvatar(message.getSender().getAvatar());
dto.setSender(userSummaryDto);
return dto;
}
public MessageConversation findOrCreateConversation(Long user1Id, Long user2Id) {
User user1 = userRepository.findById(user1Id)
.orElseThrow(() -> new IllegalArgumentException("User1 not found"));
User user2 = userRepository.findById(user2Id)
.orElseThrow(() -> new IllegalArgumentException("User2 not found"));
return findOrCreateConversation(user1, user2);
}
private MessageConversation findOrCreateConversation(User user1, User user2) {
log.info("Searching for existing conversation between {} and {}", user1.getUsername(), user2.getUsername());
return conversationRepository.findConversationByUsers(user1, user2)
.orElseGet(() -> {
log.info("No existing conversation found. Creating a new one.");
MessageConversation conversation = new MessageConversation();
conversation = conversationRepository.save(conversation);
log.info("New conversation created with ID: {}", conversation.getId());
MessageParticipant participant1 = new MessageParticipant();
participant1.setConversation(conversation);
participant1.setUser(user1);
participantRepository.save(participant1);
log.info("Participant {} added to conversation {}", user1.getUsername(), conversation.getId());
MessageParticipant participant2 = new MessageParticipant();
participant2.setConversation(conversation);
participant2.setUser(user2);
participantRepository.save(participant2);
log.info("Participant {} added to conversation {}", user2.getUsername(), conversation.getId());
return conversation;
});
}
@Transactional(readOnly = true)
public List<ConversationDto> getConversations(Long userId) {
List<MessageConversation> conversations = conversationRepository.findConversationsByUserIdOrderByLastMessageDesc(userId);
return conversations.stream().map(c -> toDto(c, userId)).collect(Collectors.toList());
}
private ConversationDto toDto(MessageConversation conversation, Long userId) {
ConversationDto dto = new ConversationDto();
dto.setId(conversation.getId());
dto.setCreatedAt(conversation.getCreatedAt());
if (conversation.getLastMessage() != null) {
dto.setLastMessage(toDto(conversation.getLastMessage()));
}
dto.setParticipants(conversation.getParticipants().stream()
.map(p -> {
UserSummaryDto userDto = new UserSummaryDto();
userDto.setId(p.getUser().getId());
userDto.setUsername(p.getUser().getUsername());
userDto.setAvatar(p.getUser().getAvatar());
return userDto;
})
.collect(Collectors.toList()));
MessageParticipant self = conversation.getParticipants().stream()
.filter(p -> p.getUser().getId().equals(userId))
.findFirst()
.orElseThrow(() -> new IllegalStateException("Participant not found in conversation"));
LocalDateTime lastRead = self.getLastReadAt() == null ? LocalDateTime.of(1970, 1, 1, 0, 0) : self.getLastReadAt();
// 只计算别人发送给当前用户的未读消息
long unreadCount = messageRepository.countByConversationIdAndCreatedAtAfterAndSenderIdNot(conversation.getId(), lastRead, userId);
dto.setUnreadCount(unreadCount);
return dto;
}
@Transactional
public ConversationDetailDto getConversationDetails(Long conversationId, Long userId, Pageable pageable) {
markConversationAsRead(conversationId, userId);
MessageConversation conversation = conversationRepository.findById(conversationId)
.orElseThrow(() -> new IllegalArgumentException("Conversation not found"));
Page<Message> messagesPage = messageRepository.findByConversationId(conversationId, pageable);
Page<MessageDto> messageDtoPage = messagesPage.map(this::toDto);
List<UserSummaryDto> participants = conversation.getParticipants().stream()
.map(p -> {
UserSummaryDto userDto = new UserSummaryDto();
userDto.setId(p.getUser().getId());
userDto.setUsername(p.getUser().getUsername());
userDto.setAvatar(p.getUser().getAvatar());
return userDto;
})
.collect(Collectors.toList());
ConversationDetailDto detailDto = new ConversationDetailDto();
detailDto.setId(conversation.getId());
detailDto.setParticipants(participants);
detailDto.setMessages(messageDtoPage);
return detailDto;
}
@Transactional
public void markConversationAsRead(Long conversationId, Long userId) {
MessageParticipant participant = participantRepository.findByConversationIdAndUserId(conversationId, userId)
.orElseThrow(() -> new IllegalArgumentException("Participant not found"));
participant.setLastReadAt(LocalDateTime.now());
participantRepository.save(participant);
}
@Transactional(readOnly = true)
public long getUnreadMessageCount(Long userId) {
List<MessageParticipant> participations = participantRepository.findByUserId(userId);
long totalUnreadCount = 0;
for (MessageParticipant p : participations) {
LocalDateTime lastRead = p.getLastReadAt() == null ? LocalDateTime.of(1970, 1, 1, 0, 0) : p.getLastReadAt();
// 只计算别人发送给当前用户的未读消息
totalUnreadCount += messageRepository.countByConversationIdAndCreatedAtAfterAndSenderIdNot(p.getConversation().getId(), lastRead, userId);
}
return totalUnreadCount;
}
}