parent
fa9627a9de
commit
70150357e6
@ -0,0 +1,14 @@
|
||||
package com.luojia_channel.common.domain.page;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ScrollPageRequest {
|
||||
private Long lastVal; // 上次查询的最小值(用于游标分页)
|
||||
private Integer offset = 0; // 偏移量(用于分页位置标记)
|
||||
private Long size = 10L; // 每页数量
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.luojia_channel.common.domain.page;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
// 滚动分页请求
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ScrollPageResponse<T> {
|
||||
private Long lastVal; // 上次查询的最小值(用于游标分页)
|
||||
private Integer offset = 0; // 偏移量(用于分页位置标记)
|
||||
private Long size = 10L; // 每页数量
|
||||
private List<T> records; // 数据列表
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,34 +1,41 @@
|
||||
package com.luojia_channel.modules.interact.controller;
|
||||
|
||||
import com.luojia_channel.modules.message.dto.MessageRequest;
|
||||
import com.luojia_channel.modules.message.server.WebSocketServer;
|
||||
import com.luojia_channel.common.domain.Result;
|
||||
import com.luojia_channel.common.domain.page.PageResponse;
|
||||
import com.luojia_channel.common.domain.page.ScrollPageResponse;
|
||||
import com.luojia_channel.modules.interact.dto.ChatItemDTO;
|
||||
import com.luojia_channel.modules.interact.dto.ChatPageQueryDTO;
|
||||
import com.luojia_channel.modules.interact.service.ChatService;
|
||||
import com.luojia_channel.modules.message.dto.MessageResponse;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/message")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "聊天模块", description = "好友聊天模块相关接口")
|
||||
public class ChatController {
|
||||
private final ChatService chatService;
|
||||
|
||||
private WebSocketServer webSocketServer;
|
||||
@Operation(
|
||||
summary = "聊天列表",
|
||||
description = "传入分页参数,查询私信用户列表(带最新消息)",
|
||||
tags = {"私信模块"}
|
||||
)
|
||||
|
||||
@GetMapping("/chat-list")
|
||||
public Result<ScrollPageResponse<ChatItemDTO>> getChatList(@RequestBody ChatPageQueryDTO chatPageQueryDTO) {
|
||||
return Result.success(chatService.getChatList(chatPageQueryDTO));
|
||||
}
|
||||
|
||||
@PostMapping("/sendPrivateMessage")
|
||||
@Operation(
|
||||
summary = "发送私信",
|
||||
description = "发送私信给指定用户",
|
||||
tags = {"聊天模块"}
|
||||
summary = "历史记录",
|
||||
description = "传入分页参数,获取与特定用户的完整聊天记录",
|
||||
tags = {"关注模块"}
|
||||
)
|
||||
public String sendPrivateMessage(@RequestParam Long senderId, @RequestBody MessageRequest request) {
|
||||
try {
|
||||
webSocketServer.sendPrivateMessage(senderId, request);
|
||||
return "私信发送成功";
|
||||
} catch (Exception e) {
|
||||
return "私信发送失败: " + e.getMessage();
|
||||
}
|
||||
@GetMapping("/history")
|
||||
public Result<ScrollPageResponse<MessageResponse>> getChatHistory(@RequestBody ChatPageQueryDTO chatPageQueryDTO) {
|
||||
return Result.success(chatService.getChatHistory(chatPageQueryDTO));
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package com.luojia_channel.modules.interact.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@Schema(description = "聊天列表项DTO")
|
||||
public class ChatItemDTO {
|
||||
@Schema(
|
||||
description = "聊天对象的用户ID",
|
||||
required = true,
|
||||
example = "123456"
|
||||
)
|
||||
private Long chatUserId;
|
||||
|
||||
@Schema(
|
||||
description = "聊天对象的头像URL",
|
||||
example = "https://example.com/avatar.jpg"
|
||||
)
|
||||
private String avatar;
|
||||
|
||||
@Schema(
|
||||
description = "聊天对象的用户名",
|
||||
required = true,
|
||||
example = "张三"
|
||||
)
|
||||
private String username;
|
||||
|
||||
@Schema(
|
||||
description = "最新消息内容",
|
||||
required = true,
|
||||
maxLength = 500,
|
||||
example = "今天下午开会"
|
||||
)
|
||||
private String latestMessage;
|
||||
|
||||
@Schema(
|
||||
description = "最新消息时间",
|
||||
required = true,
|
||||
example = "2023-10-15T14:30:00"
|
||||
)
|
||||
private LocalDateTime latestTime;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.luojia_channel.modules.interact.dto;
|
||||
|
||||
import com.luojia_channel.common.domain.page.ScrollPageRequest;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ChatPageQueryDTO extends ScrollPageRequest {
|
||||
private Long chatUserId;
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.luojia_channel.modules.interact.service;
|
||||
|
||||
import com.luojia_channel.common.domain.page.PageResponse;
|
||||
import com.luojia_channel.common.domain.page.ScrollPageResponse;
|
||||
import com.luojia_channel.modules.interact.dto.ChatItemDTO;
|
||||
import com.luojia_channel.modules.interact.dto.ChatPageQueryDTO;
|
||||
import com.luojia_channel.modules.message.dto.MessageResponse;
|
||||
import com.luojia_channel.modules.message.entity.MessageDO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ChatService {
|
||||
|
||||
ScrollPageResponse<ChatItemDTO> getChatList(ChatPageQueryDTO chatPageQueryDTO);
|
||||
|
||||
ScrollPageResponse<MessageResponse> getChatHistory(ChatPageQueryDTO chatPageQueryDTO);
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
package com.luojia_channel.modules.interact.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.luojia_channel.common.domain.page.ScrollPageResponse;
|
||||
import com.luojia_channel.common.utils.PageUtil;
|
||||
import com.luojia_channel.common.utils.RedisUtil;
|
||||
import com.luojia_channel.common.utils.UserContext;
|
||||
import com.luojia_channel.modules.interact.dto.ChatItemDTO;
|
||||
import com.luojia_channel.modules.interact.dto.ChatPageQueryDTO;
|
||||
import com.luojia_channel.modules.interact.service.ChatService;
|
||||
import com.luojia_channel.modules.message.dto.MessageResponse;
|
||||
import com.luojia_channel.modules.message.entity.MessageDO;
|
||||
import com.luojia_channel.modules.message.mapper.MessageMapper;
|
||||
import com.luojia_channel.modules.user.entity.User;
|
||||
import com.luojia_channel.modules.user.mapper.UserMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ChatServiceImpl implements ChatService {
|
||||
|
||||
private final MessageMapper messageMapper;
|
||||
private final UserMapper userMapper;
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
@Override
|
||||
public ScrollPageResponse<ChatItemDTO> getChatList(ChatPageQueryDTO chatPageQueryDTO) {
|
||||
/*
|
||||
Long userId = UserContext.getUserId();
|
||||
IPage<ChatItemDTO> chatPage = messageMapper.selectChatList(PageUtil.convert(chatPageQueryDTO), userId);
|
||||
return PageResponse.<ChatItemDTO>builder()
|
||||
.current(chatPage.getCurrent())
|
||||
.size(chatPage.getSize())
|
||||
.total(chatPage.getTotal())
|
||||
.records(chatPage.getRecords())
|
||||
.build();
|
||||
*/
|
||||
Long userId = UserContext.getUserId();
|
||||
String key = "chat:user_list:" + userId;
|
||||
|
||||
return redisUtil.scrollPageQuery(key, ChatItemDTO.class, chatPageQueryDTO,
|
||||
(chatUserIds) -> {
|
||||
List<ChatItemDTO> chatItems = new ArrayList<>();
|
||||
List<Long> latestMessageIds = new ArrayList<>();
|
||||
List<User> users = userMapper.selectByIdsOrderByField(chatUserIds);
|
||||
for(Long chatUserId : chatUserIds){
|
||||
String messageKey = "chat:history:" + Math.min(userId, chatUserId) + ":" +Math.max(userId, chatUserId);
|
||||
// 获取zset中最新的messageId
|
||||
Long latestMessageId = redisUtil.zRevMaxValue(messageKey);
|
||||
latestMessageIds.add(latestMessageId);
|
||||
}
|
||||
List<MessageDO> messageDOS = messageMapper.selectByIdsOrderByField(latestMessageIds);
|
||||
int i=0;
|
||||
for(User user : users){
|
||||
ChatItemDTO chatItemDTO = ChatItemDTO.builder()
|
||||
.chatUserId(user.getId())
|
||||
.avatar(user.getAvatar())
|
||||
.username(user.getUsername())
|
||||
.latestMessage(messageDOS.get(i).getContent())
|
||||
.latestTime(messageDOS.get(i).getCreateTime())
|
||||
.build();
|
||||
chatItems.add(chatItemDTO);
|
||||
i++;
|
||||
}
|
||||
|
||||
return chatItems;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public ScrollPageResponse<MessageResponse> getChatHistory(ChatPageQueryDTO chatPageQueryDTO) {
|
||||
/*
|
||||
Long userId = UserContext.getUserId();
|
||||
Long chatUserId = chatPageQueryDTO.getChatUserId();
|
||||
LambdaQueryWrapper<MessageDO> queryWrapper = Wrappers.lambdaQuery(MessageDO.class)
|
||||
.eq(MessageDO::getSenderId, userId)
|
||||
.eq(MessageDO::getReceiverId, chatUserId)
|
||||
.or()
|
||||
.eq(MessageDO::getReceiverId, userId)
|
||||
.eq(MessageDO::getSenderId, chatUserId)
|
||||
.orderByDesc(MessageDO::getCreateTime);
|
||||
// 查询的是私信消息
|
||||
queryWrapper.eq(MessageDO::getMessageType, 1);
|
||||
IPage<MessageDO> page = messageMapper.selectPage(PageUtil.convert(chatPageQueryDTO), queryWrapper);
|
||||
User chatUser = userMapper.selectById(chatUserId);
|
||||
return PageUtil.convert(page, (message) -> {
|
||||
MessageResponse messageResponse = BeanUtil.copyProperties(message, MessageResponse.class);
|
||||
if(messageResponse.getSenderId().equals(userId)) {
|
||||
messageResponse.setSenderAvatar(UserContext.getAvatar());
|
||||
messageResponse.setSenderName(UserContext.getUsername());
|
||||
}else{
|
||||
messageResponse.setSenderAvatar(chatUser.getAvatar());
|
||||
messageResponse.setSenderName(chatUser.getUsername());
|
||||
}
|
||||
return messageResponse;
|
||||
});
|
||||
*/
|
||||
|
||||
// 改成滚动分页查询
|
||||
Long userId = UserContext.getUserId();
|
||||
Long chatUserId = chatPageQueryDTO.getChatUserId();
|
||||
String key = "chat:history:" + Math.min(userId, chatUserId) + ":" +Math.max(userId, chatUserId);
|
||||
return redisUtil.scrollPageQuery(key, MessageResponse.class, chatPageQueryDTO,
|
||||
(messageIds) -> {
|
||||
List<MessageDO> messageDOS = messageMapper.selectByIdsOrderByField(messageIds);
|
||||
User chatUser = userMapper.selectById(chatUserId);
|
||||
List<MessageResponse> messageResponses = new ArrayList<>();
|
||||
for(MessageDO message : messageDOS){
|
||||
MessageResponse messageResponse = BeanUtil.copyProperties(message, MessageResponse.class);
|
||||
if(messageResponse.getSenderId().equals(userId)) {
|
||||
messageResponse.setSenderAvatar(UserContext.getAvatar());
|
||||
messageResponse.setSenderName(UserContext.getUsername());
|
||||
}else{
|
||||
messageResponse.setSenderAvatar(chatUser.getAvatar());
|
||||
messageResponse.setSenderName(chatUser.getUsername());
|
||||
}
|
||||
messageResponses.add(messageResponse);
|
||||
}
|
||||
return messageResponses;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
package com.luojia_channel.modules.message.util;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class WebSocketContext implements ApplicationContextAware {
|
||||
private static ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
@Autowired
|
||||
public void setApplicationContext(ApplicationContext inApplicationContext) throws BeansException {
|
||||
applicationContext = inApplicationContext;
|
||||
}
|
||||
|
||||
public static ApplicationContext getApplicationContext() {
|
||||
return applicationContext;
|
||||
}
|
||||
|
||||
public static Object getBean(String name) {
|
||||
return getApplicationContext().getBean(name);
|
||||
}
|
||||
|
||||
public static <T> T getBean(Class<T> clazz) {
|
||||
return getApplicationContext().getBean(clazz);
|
||||
}
|
||||
|
||||
public static <T> T getBean(String name, Class<T> clazz) {
|
||||
return getApplicationContext().getBean(name, clazz);
|
||||
}
|
||||
|
||||
|
||||
public static String getActiveProfile() {
|
||||
String[] activeProfiles = getApplicationContext().getEnvironment().getActiveProfiles();
|
||||
if (activeProfiles.length == 0) {
|
||||
return null;
|
||||
}
|
||||
return activeProfiles[0];
|
||||
}
|
||||
}
|
@ -1,15 +1,20 @@
|
||||
package com.luojia_channel.modules.post.dto.req;
|
||||
|
||||
import com.luojia_channel.common.domain.page.PageRequest;
|
||||
import com.luojia_channel.common.domain.page.ScrollPageRequest;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(title = "分页查询评论请求DTO")
|
||||
public class CommentPageQueryDTO extends PageRequest {
|
||||
public class CommentPageQueryDTO extends ScrollPageRequest {
|
||||
@Schema(title = "帖子ID")
|
||||
private Long postId;
|
||||
|
||||
@Schema(title = "评论ID")
|
||||
private Long commentId;
|
||||
private Long parentCommentId;
|
||||
|
||||
private Boolean orderByTime = true;
|
||||
|
||||
private Boolean orderByHot = false;
|
||||
}
|
||||
|
@ -1,36 +1,36 @@
|
||||
#本地开发环境
|
||||
lj:
|
||||
db:
|
||||
host: localhost
|
||||
password: 123456
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: 123456
|
||||
rabbitmq:
|
||||
host: localhost
|
||||
port: 15672
|
||||
username: root
|
||||
password: 123456
|
||||
minio:
|
||||
endpoint: http://localhost:9000
|
||||
accessKey: minioadmin
|
||||
secretKey: minioadmin
|
||||
# lj:
|
||||
# db:
|
||||
# host: localhost
|
||||
# password: 123456
|
||||
# redis:
|
||||
# host: localhost
|
||||
# port: 6379
|
||||
# password: 123456
|
||||
# rabbitmq:
|
||||
# host: localhost
|
||||
# port: 15672
|
||||
# username: root
|
||||
# password: 123456
|
||||
# minio:
|
||||
# endpoint: http://localhost:9000
|
||||
# accessKey: minioadmin
|
||||
# secretKey: minioadmin
|
||||
|
||||
#lj:
|
||||
# db:
|
||||
# host: 192.168.59.129
|
||||
# password: Forely123!
|
||||
# redis:
|
||||
# host: 192.168.59.129
|
||||
# port: 6379
|
||||
# password: Forely123!
|
||||
# rabbitmq:
|
||||
# host: 192.168.59.129
|
||||
# port: 5672
|
||||
# username: admin
|
||||
# password: Forely123!
|
||||
# minio:
|
||||
# endpoint: http://192.168.59.129:9000
|
||||
# accessKey: forely
|
||||
# secretKey: Forely123!
|
||||
lj:
|
||||
db:
|
||||
host: 192.168.59.129
|
||||
password: Forely123!
|
||||
redis:
|
||||
host: 192.168.59.129
|
||||
port: 6379
|
||||
password: Forely123!
|
||||
rabbitmq:
|
||||
host: 192.168.59.129
|
||||
port: 5672
|
||||
username: admin
|
||||
password: Forely123!
|
||||
minio:
|
||||
endpoint: http://192.168.59.129:9000
|
||||
accessKey: forely
|
||||
secretKey: Forely123!
|
||||
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="com.luojia_channel.modules.post.mapper.CommentMapper">
|
||||
<select id="selectByIdsOrderByField" resultType="com.luojia_channel.modules.post.entity.Comment">
|
||||
SELECT * FROM comment
|
||||
WHERE id IN
|
||||
<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
|
||||
ORDER BY FIELD(id,
|
||||
<foreach item="id" collection="ids" separator="," open="" close="">#{id}</foreach>)
|
||||
</select>
|
||||
</mapper>
|
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="com.luojia_channel.modules.post.mapper.PostMapper">
|
||||
<select id="selectByIdsOrderByField" resultType="com.luojia_channel.modules.post.entity.Post">
|
||||
SELECT * FROM post
|
||||
WHERE id IN
|
||||
<foreach item="id" collection="ids" open="(" separator="," close=")">#{id}</foreach>
|
||||
ORDER BY FIELD(id,
|
||||
<foreach item="id" collection="ids" separator="," open="" close="">#{id}</foreach>)
|
||||
</select>
|
||||
</mapper>
|
@ -1,36 +1,36 @@
|
||||
#本地开发环境
|
||||
lj:
|
||||
db:
|
||||
host: localhost
|
||||
password: 123456
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: 123456
|
||||
rabbitmq:
|
||||
host: localhost
|
||||
port: 15672
|
||||
username: root
|
||||
password: 123456
|
||||
minio:
|
||||
endpoint: http://localhost:9000
|
||||
accessKey: minioadmin
|
||||
secretKey: minioadmin
|
||||
# lj:
|
||||
# db:
|
||||
# host: localhost
|
||||
# password: 123456
|
||||
# redis:
|
||||
# host: localhost
|
||||
# port: 6379
|
||||
# password: 123456
|
||||
# rabbitmq:
|
||||
# host: localhost
|
||||
# port: 15672
|
||||
# username: root
|
||||
# password: 123456
|
||||
# minio:
|
||||
# endpoint: http://localhost:9000
|
||||
# accessKey: minioadmin
|
||||
# secretKey: minioadmin
|
||||
|
||||
#lj:
|
||||
# db:
|
||||
# host: 192.168.59.129
|
||||
# password: Forely123!
|
||||
# redis:
|
||||
# host: 192.168.59.129
|
||||
# port: 6379
|
||||
# password: Forely123!
|
||||
# rabbitmq:
|
||||
# host: 192.168.59.129
|
||||
# port: 5672
|
||||
# username: admin
|
||||
# password: Forely123!
|
||||
# minio:
|
||||
# endpoint: http://192.168.59.129:9000
|
||||
# accessKey: forely
|
||||
# secretKey: Forely123!
|
||||
lj:
|
||||
db:
|
||||
host: 192.168.59.129
|
||||
password: Forely123!
|
||||
redis:
|
||||
host: 192.168.59.129
|
||||
port: 6379
|
||||
password: Forely123!
|
||||
rabbitmq:
|
||||
host: 192.168.59.129
|
||||
port: 5672
|
||||
username: admin
|
||||
password: Forely123!
|
||||
minio:
|
||||
endpoint: http://192.168.59.129:9000
|
||||
accessKey: forely
|
||||
secretKey: Forely123!
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue