Skip to content

Commit

Permalink
Merge pull request #69 from L1nkWave/feature/LWB-45_messages-pagination
Browse files Browse the repository at this point in the history
feature/LWB-45_messages-pagination
  • Loading branch information
borjom1 committed May 15, 2024
2 parents 8984f04 + 088c818 commit 5954c89
Show file tree
Hide file tree
Showing 14 changed files with 433 additions and 80 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package org.linkwave.chatservice.api.users;

import org.linkwave.chatservice.common.RequestInitiator;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toMap;

@FeignClient(value = "user-service", path = "/api/v1/users")
public interface UserServiceClient {
Expand All @@ -25,9 +32,25 @@ List<UserDto> getUsers(

@GetMapping("/contacts")
List<ContactDto> getContacts(
@RequestParam String username,
@RequestParam String search,
@RequestParam int offset, @RequestParam int limit,
@RequestHeader("Authorization") String authHeader
);

default Map<Long, ContactDto> fetchAllContacts(@NonNull RequestInitiator initiator, int batchSize) {
final String username = ""; // any username is matched
int offset = 0;

final List<ContactDto> allContacts = new LinkedList<>();
List<ContactDto> contacts;

do {
contacts = getContacts(username, offset, batchSize, initiator.bearer());
allContacts.addAll(contacts);
offset += batchSize;
} while (contacts.size() == batchSize);
return allContacts.stream()
.collect(toMap(contact -> contact.getUser().getId(), identity()));
}

}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package org.linkwave.chatservice.chat;

import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.*;

import java.time.Instant;

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import lombok.extern.slf4j.Slf4j;
import org.linkwave.chatservice.api.ApiResponseClientErrorException;
import org.linkwave.chatservice.api.ServiceErrorException;
import org.linkwave.chatservice.api.users.ContactDto;
import org.linkwave.chatservice.api.users.UserDto;
import org.linkwave.chatservice.api.users.UserServiceClient;
import org.linkwave.chatservice.api.ws.LoadChatRequest;
Expand Down Expand Up @@ -207,64 +206,44 @@ public Pair<Long, List<ChatDto>> getUserChats(@NonNull RequestInitiator initiato
.forEach(user -> usersMap.put(user.getId(), user))
);

// pull contacts
final Map<Long, ContactDto> contactsMap = fetchAllContacts(initiator);
// pull contacts & set aliases
userServiceClient.fetchAllContacts(initiator, DEFAULT_BATCH_SIZE)
.forEach((userId, dto) -> {
final UserDto userDto = usersMap.get(userId);
if (userDto != null) {
userDto.setName(dto.getAlias());
}
});

selectedChats.forEach(chat -> {
final MessageDto lastMessage = chat.getLastMessage();
if (lastMessage == null) {
return;
}
final MessageAuthorDto author = lastMessage.getAuthor();
final UserDto user = usersMap.get(author.getId());
if (user != null) { // if user is found, add user details
author.setUsername(user.getUsername());
author.setName(user.getName());
author.setDeleted(user.isDeleted());

if (chat instanceof DuoChatDto duoChat) {
duoChat.setAvatarAvailable(user.getAvatarPath() != null);

// inject companion dto
final Long companionId = duoChat.getUser().getId();
final UserDto userDto = usersMap.get(companionId);
if (userDto != null) {
duoChat.setUser(modelMapper.map(userDto, CompanionDto.class));
}
}
if (chat instanceof DuoChatDto duoChat) {
final Long companionId = duoChat.getUser().getId();
final UserDto user = usersMap.get(companionId);

// try to replace with contact alias
if (!user.getId().equals(initiator.userId())) {
final ContactDto contact = contactsMap.get(user.getId());
if (contact != null) {
author.setUsername(contact.getAlias());
}
if (user != null) {
duoChat.setAvatarAvailable(user.getAvatarPath() != null);
duoChat.setUser(modelMapper.map(user, CompanionDto.class));
}
} else { // add author details for group chats only
final UserDto user = usersMap.get(author.getId());
author.setUsername(user.getUsername());
author.setName(user.getName());
author.setIsDeleted(user.isDeleted());
}

});

log.debug("-> getUserChats(): performed {} api-requests", batches);

return Pair.of(chatsTotalCount, selectedChats);
}

@NonNull
private Map<Long, ContactDto> fetchAllContacts(@NonNull RequestInitiator initiator) {
final String username = ""; // any username is matched
int offset = 0;

final List<ContactDto> allContacts = new LinkedList<>();
List<ContactDto> contacts;

do {
contacts = userServiceClient.getContacts(username, offset, DEFAULT_BATCH_SIZE, initiator.bearer());
allContacts.addAll(contacts);
offset += DEFAULT_BATCH_SIZE;
} while (contacts.size() == DEFAULT_BATCH_SIZE);
return allContacts.stream()
.collect(toMap(contact -> contact.getUser().getId(), identity()));
}

private ChatDto mapChats(Chat chat, Set<Long> usersIds, RequestInitiator initiator) {
final boolean isGroupChat = chat instanceof GroupChat;
final Class<? extends ChatDto> cls = isGroupChat
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ public class MessageAuthorDto {
private String name;

@JsonProperty("deleted")
private boolean isDeleted;
private Boolean isDeleted;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.linkwave.chatservice.message;

import org.linkwave.chatservice.api.users.UserDto;
import org.modelmapper.ModelMapper;
import org.springframework.lang.NonNull;

import java.util.Map;

public interface FetchMessageMapping {

MessageDto mapForFetch(@NonNull ModelMapper modelMapper, Long fetcherUserId, Map<Long, UserDto> users);

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.querydsl.core.annotations.QueryEntity;
import lombok.*;
import lombok.experimental.SuperBuilder;
import org.linkwave.chatservice.api.users.UserDto;
import org.linkwave.chatservice.chat.MessageAuthorDto;
import org.linkwave.chatservice.chat.duo.Chat;
import org.linkwave.chatservice.common.DtoConverter;
import org.modelmapper.ModelMapper;
Expand All @@ -14,6 +16,7 @@
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@QueryEntity
@Document("messages")
Expand All @@ -22,7 +25,7 @@
@EqualsAndHashCode(of = {"id", "action", "createdAt", "authorId"})
@ToString(exclude = "chat")
@SuperBuilder
public class Message implements DtoConverter<MessageDto> {
public class Message implements DtoConverter<MessageDto>, FetchMessageMapping {

private String id;
private Action action;
Expand All @@ -46,4 +49,22 @@ public MessageDto convert(@NonNull ModelMapper modelMapper) {
return modelMapper.map(this, MessageDto.class);
}

@Override
public MessageDto mapForFetch(@NonNull ModelMapper modelMapper, Long fetcherUserId,
@NonNull Map<Long, UserDto> users) {
final MessageDto message = convert(modelMapper);
final Long authorId = message.getAuthor().getId();
final UserDto authorUserDto = users.get(authorId);

if (authorUserDto != null) {
final MessageAuthorDto messageAuthorDto = modelMapper.map(authorUserDto, MessageAuthorDto.class);
messageAuthorDto.setIsDeleted(authorUserDto.isDeleted());
message.setAuthor(messageAuthorDto);
}
if (authorId.equals(fetcherUserId)) {
message.setIsRead(this.isRead);
}

return message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

import com.fasterxml.jackson.annotation.JsonView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.linkwave.chatservice.common.DtoViews.New;
import org.linkwave.chatservice.common.UnacceptableRequestDataException;
import org.linkwave.chatservice.message.file.CreatedFileMessage;
import org.linkwave.chatservice.message.text.EditTextMessage;
import org.linkwave.chatservice.message.text.NewTextMessage;
import org.linkwave.chatservice.message.text.UpdatedTextMessage;
import org.springframework.data.util.Pair;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.*;
Expand All @@ -20,9 +23,10 @@

import static org.linkwave.chatservice.common.RequestUtils.requestInitiator;
import static org.linkwave.chatservice.common.RequestUtils.userDetails;
import static org.linkwave.shared.utils.Headers.TOTAL_COUNT;
import static org.springframework.http.HttpStatus.*;
import static org.springframework.http.ResponseEntity.ok;

@Slf4j
@RestController
@RequestMapping("/api/v1/chats")
@RequiredArgsConstructor
Expand Down Expand Up @@ -85,8 +89,17 @@ public byte[] getAttachedFile(@PathVariable String messageId) {
}

@GetMapping("/{chatId}/messages")
public ResponseEntity<List<Message>> getMessages(@PathVariable String chatId) {
return ok(messageService.getChatMessages(userDetails().id(), chatId));
public List<MessageDto> getMessages(
@PathVariable String chatId, @RequestParam int offset, @RequestParam int limit,
@NonNull HttpServletRequest request, @NonNull HttpServletResponse response
) {
log.debug("-> getMessages(): offset = {}, limit={}", offset, limit);

final Pair<Long, List<MessageDto>> chatMessages = messageService.getChatMessages(
requestInitiator(request), chatId, offset, limit
);
response.addHeader(TOTAL_COUNT.getValue(), String.valueOf(chatMessages.getFirst()));
return chatMessages.getSecond();
}

@PatchMapping("/messages/{id}/text")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class MessageDto {

@JsonProperty("isRead")
@JsonView({Detailed.class})
private boolean isRead;
private Boolean isRead;

@JsonView({Detailed.class})
private List<MessageReaction> reactions;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
package org.linkwave.chatservice.message;

import org.springframework.data.mongodb.repository.Aggregation;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface MessageRepository extends MongoRepository<Message, String>, QuerydslPredicateExecutor<Message> {

@Query(value = "{ 'chat.$id': { $eq: ObjectId(?0) } }", delete = true)
void deleteAllChatMessages(String chatId);

@Aggregation(
pipeline = {"""
{
$match: {
'chat.$id': { $eq: ObjectId(?0) }
}
}
""",
"{ $sort: { createdAt: -1 } }",
"{ $skip : ?1 }",
"{ $limit : ?2 }"
}
)
List<Message> getMessages(String chatId, int offset, int limit);

@Query(value = "{ 'chat.$id': { $eq: ObjectId(?0) } }", count = true)
long getChatMessagesCount(String chatId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.linkwave.chatservice.message.text.NewTextMessage;
import org.linkwave.chatservice.message.text.UpdatedTextMessage;
import org.linkwave.chatservice.user.User;
import org.springframework.data.util.Pair;
import org.springframework.lang.NonNull;
import org.springframework.web.multipart.MultipartFile;

Expand Down Expand Up @@ -37,7 +38,7 @@ public interface MessageService {

boolean isMessageSender(Message message, Long memberId);

List<Message> getChatMessages(Long userId, String chatId);
Pair<Long, List<MessageDto>> getChatMessages(RequestInitiator initiator, String chatId, int offset, int limit);

ReadMessages readMessages(Long memberId, String chatId, Instant lastReadMessageTimestamp);

Expand Down
Loading

0 comments on commit 5954c89

Please sign in to comment.