Browse Source

!29 消息已读未读显示

Merge pull request !29 from blue/v_2.0.0
master
blue 2 years ago
committed by Gitee
parent
commit
41f276786a
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 10
      im-client/src/main/java/com/bx/imclient/sender/IMSender.java
  2. 4
      im-commom/src/main/java/com/bx/imcommon/contant/IMRedisKey.java
  3. 11
      im-commom/src/main/java/com/bx/imcommon/enums/IMCmdType.java
  4. 12
      im-commom/src/main/java/com/bx/imcommon/enums/IMListenerType.java
  5. 10
      im-commom/src/main/java/com/bx/imcommon/enums/IMSendCode.java
  6. 11
      im-commom/src/main/java/com/bx/imcommon/enums/IMTerminalType.java
  7. 6
      im-commom/src/main/java/com/bx/imcommon/model/IMGroupMessage.java
  8. 18
      im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java
  9. 15
      im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java
  10. 8
      im-platform/src/main/java/com/bx/implatform/controller/UserController.java
  11. 12
      im-platform/src/main/java/com/bx/implatform/enums/FileType.java
  12. 17
      im-platform/src/main/java/com/bx/implatform/enums/MessageStatus.java
  13. 11
      im-platform/src/main/java/com/bx/implatform/enums/MessageType.java
  14. 23
      im-platform/src/main/java/com/bx/implatform/enums/ResultCode.java
  15. 1
      im-platform/src/main/java/com/bx/implatform/listener/GroupMessageListener.java
  16. 4
      im-platform/src/main/java/com/bx/implatform/listener/PrivateMessageListener.java
  17. 4
      im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java
  18. 3
      im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java
  19. 2
      im-platform/src/main/java/com/bx/implatform/service/IUserService.java
  20. 2
      im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java
  21. 184
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  22. 102
      im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
  23. 12
      im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java
  24. 16
      im-platform/src/main/java/com/bx/implatform/service/thirdparty/FileService.java
  25. 5
      im-platform/src/main/java/com/bx/implatform/util/MinioUtil.java
  26. 28
      im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java
  27. 30
      im-platform/src/main/java/com/bx/implatform/vo/PrivateMessageVO.java
  28. 2
      im-server/src/main/java/com/bx/imserver/netty/tcp/TcpSocketServer.java
  29. 2
      im-server/src/main/java/com/bx/imserver/netty/ws/WebSocketServer.java
  30. 2
      im-server/src/main/java/com/bx/imserver/task/PullUnreadGroupMessageTask.java
  31. 2
      im-server/src/main/java/com/bx/imserver/task/PullUnreadPrivateMessageTask.java
  32. 3
      im-ui/src/api/emotion.js
  33. 12
      im-ui/src/api/enums.js
  34. 252
      im-ui/src/components/chat/ChatBox.vue
  35. 60
      im-ui/src/components/chat/ChatMessageItem.vue
  36. 2
      im-ui/src/components/common/FileUpload.vue
  37. 60
      im-ui/src/components/common/FullImage.vue
  38. 158
      im-ui/src/store/chatStore.js
  39. 41
      im-ui/src/store/friendStore.js
  40. 56
      im-ui/src/store/groupStore.js
  41. 27
      im-ui/src/store/index.js
  42. 1
      im-ui/src/store/uiStore.js
  43. 28
      im-ui/src/store/userStore.js
  44. 30
      im-ui/src/view/Chat.vue
  45. 269
      im-ui/src/view/Home.vue
  46. 170
      im-ui/src/view/Login.vue
  47. 25
      im-ui/src/view/Register.vue
  48. 176
      im-uniapp/App.vue
  49. 23
      im-uniapp/common/emotion.js
  50. 11
      im-uniapp/common/enums.js
  51. 3
      im-uniapp/common/request.js
  52. 57
      im-uniapp/components/chat-message-item/chat-message-item.vue
  53. 91
      im-uniapp/components/loading/loading.vue
  54. 86
      im-uniapp/pages/chat/chat-box.vue
  55. 26
      im-uniapp/pages/chat/chat.vue
  56. 9
      im-uniapp/pages/login/login.vue
  57. 141
      im-uniapp/store/chatStore.js

10
im-client/src/main/java/com/bx/imclient/sender/IMSender.java

@ -14,8 +14,6 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Service
public class IMSender {
@ -34,7 +32,7 @@ public class IMSender {
Integer serverId = (Integer)redisTemplate.opsForValue().get(key);
// 如果对方在线,将数据存储至redis,等待拉取推送
if (serverId != null) {
String sendKey = String.join(":", IMRedisKey.IM_UNREAD_PRIVATE_QUEUE, serverId.toString());
String sendKey = String.join(":", IMRedisKey.IM_MESSAGE_PRIVATE_QUEUE, serverId.toString());
IMRecvInfo recvInfo = new IMRecvInfo();
recvInfo.setCmd(IMCmdType.PRIVATE_MESSAGE.code());
recvInfo.setSendResult(message.getSendResult());
@ -63,7 +61,7 @@ public class IMSender {
Integer serverId = (Integer)redisTemplate.opsForValue().get(key);
// 如果终端在线,将数据存储至redis,等待拉取推送
if (serverId != null) {
String sendKey = String.join(":", IMRedisKey.IM_UNREAD_PRIVATE_QUEUE, serverId.toString());
String sendKey = String.join(":", IMRedisKey.IM_MESSAGE_PRIVATE_QUEUE, serverId.toString());
IMRecvInfo recvInfo = new IMRecvInfo();
// 自己的消息不需要回推消息结果
recvInfo.setSendResult(false);
@ -112,7 +110,7 @@ public class IMSender {
recvInfo.setSendResult(message.getSendResult());
recvInfo.setData(message.getData());
// 推送至队列
String key = String.join(":", IMRedisKey.IM_UNREAD_GROUP_QUEUE, entry.getKey().toString());
String key = String.join(":", IMRedisKey.IM_MESSAGE_GROUP_QUEUE, entry.getKey().toString());
redisTemplate.opsForList().rightPush(key, recvInfo);
}
// 对离线用户回复消息状态
@ -144,7 +142,7 @@ public class IMSender {
// 自己的消息不需要回推消息结果
recvInfo.setSendResult(false);
recvInfo.setData(message.getData());
String sendKey = String.join(":", IMRedisKey.IM_UNREAD_GROUP_QUEUE, serverId.toString());
String sendKey = String.join(":", IMRedisKey.IM_MESSAGE_GROUP_QUEUE, serverId.toString());
redisTemplate.opsForList().rightPush(sendKey, recvInfo);
}
}

4
im-commom/src/main/java/com/bx/imcommon/contant/IMRedisKey.java

@ -7,9 +7,9 @@ public class IMRedisKey {
// 用户ID所连接的IM-server的ID
public final static String IM_USER_SERVER_ID = "im:user:server_id";
// 未读私聊消息队列
public final static String IM_UNREAD_PRIVATE_QUEUE = "im:unread:private";
public final static String IM_MESSAGE_PRIVATE_QUEUE = "im:message:private";
// 未读群聊消息队列
public final static String IM_UNREAD_GROUP_QUEUE = "im:unread:group";
public final static String IM_MESSAGE_GROUP_QUEUE = "im:message:group";
// 私聊消息发送结果队列
public final static String IM_RESULT_PRIVATE_QUEUE = "im:result:private";
// 群聊消息发送结果队列

11
im-commom/src/main/java/com/bx/imcommon/enums/IMCmdType.java

@ -1,7 +1,9 @@
package com.bx.imcommon.enums;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum IMCmdType {
LOGIN(0,"登陆"),
@ -11,15 +13,10 @@ public enum IMCmdType {
GROUP_MESSAGE(4,"群发消息");
private Integer code;
private String desc;
IMCmdType(Integer index, String desc) {
this.code =index;
this.desc=desc;
}
public static IMCmdType fromCode(Integer code){
for (IMCmdType typeEnum:values()) {
@ -31,10 +28,6 @@ public enum IMCmdType {
}
public String description() {
return desc;
}
public Integer code(){
return this.code;
}

12
im-commom/src/main/java/com/bx/imcommon/enums/IMListenerType.java

@ -1,5 +1,8 @@
package com.bx.imcommon.enums;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum IMListenerType{
ALL(0,"全部消息"),
PRIVATE_MESSAGE(1,"私聊消息"),
@ -9,15 +12,6 @@ public enum IMListenerType{
private String desc;
IMListenerType(Integer index, String desc) {
this.code =index;
this.desc=desc;
}
public String description() {
return desc;
}
public Integer code(){

10
im-commom/src/main/java/com/bx/imcommon/enums/IMSendCode.java

@ -1,6 +1,8 @@
package com.bx.imcommon.enums;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum IMSendCode {
SUCCESS(0,"发送成功"),
@ -11,11 +13,6 @@ public enum IMSendCode {
private Integer code;
private String desc;
// 构造方法
IMSendCode(int code, String desc) {
this.code = code;
this.desc = desc;
}
public static IMSendCode fromCode(Integer code){
for (IMSendCode typeEnum:values()) {
@ -27,9 +24,6 @@ public enum IMSendCode {
}
public String description() {
return desc;
}
public Integer code(){

11
im-commom/src/main/java/com/bx/imcommon/enums/IMTerminalType.java

@ -1,9 +1,12 @@
package com.bx.imcommon.enums;
import lombok.AllArgsConstructor;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@AllArgsConstructor
public enum IMTerminalType {
WEB(0,"web"),
@ -13,10 +16,6 @@ public enum IMTerminalType {
private String desc;
IMTerminalType(Integer index, String desc) {
this.code =index;
this.desc=desc;
}
public static IMTerminalType fromCode(Integer code){
for (IMTerminalType typeEnum:values()) {
@ -31,10 +30,6 @@ public enum IMTerminalType {
return Arrays.stream(values()).map(IMTerminalType::code).collect(Collectors.toList());
}
public String description() {
return desc;
}
public Integer code(){
return this.code;
}

6
im-commom/src/main/java/com/bx/imcommon/model/IMGroupMessage.java

@ -3,6 +3,8 @@ package com.bx.imcommon.model;
import com.bx.imcommon.enums.IMTerminalType;
import lombok.Data;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@Data
@ -14,9 +16,9 @@ public class IMGroupMessage<T> {
private IMUserInfo sender;
/**
* 接收者id列表(群成员列表)
* 接收者id列表(群成员列表,为空则不会推送)
*/
private List<Long> recvIds;
private List<Long> recvIds = Collections.EMPTY_LIST;
/**

18
im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java

@ -6,6 +6,7 @@ import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils;
import com.bx.implatform.service.IGroupMessageService;
import com.bx.implatform.dto.GroupMessageDTO;
import com.bx.implatform.vo.PrivateMessageVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
@ -38,6 +39,7 @@ public class GroupMessageController {
return ResultUtils.success();
}
// todo 删除
@PostMapping("/pullUnreadMessage")
@ApiOperation(value = "拉取未读消息",notes="拉取未读消息")
public Result pullUnreadMessage(){
@ -45,6 +47,22 @@ public class GroupMessageController {
return ResultUtils.success();
}
@GetMapping("/loadMessage")
@ApiOperation(value = "拉取消息",notes="拉取消息,一次最多拉取100条")
public Result<List<GroupMessageVO>> loadMessage(@RequestParam Long minId){
return ResultUtils.success(groupMessageService.loadMessage(minId));
}
@PutMapping("/readed")
@ApiOperation(value = "消息已读",notes="将群聊中的消息状态置为已读")
public Result readedMessage(@RequestParam Long groupId){
groupMessageService.readedMessage(groupId);
return ResultUtils.success();
}
@GetMapping("/history")
@ApiOperation(value = "查询聊天记录",notes="查询聊天记录")
public Result<List<GroupMessageVO>> recallMessage(@NotNull(message = "群聊id不能为空") @RequestParam Long groupId,

15
im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java

@ -37,7 +37,7 @@ public class PrivateMessageController {
return ResultUtils.success();
}
// todo 删除
@PostMapping("/pullUnreadMessage")
@ApiOperation(value = "拉取未读消息",notes="拉取未读消息")
public Result pullUnreadMessage(){
@ -46,6 +46,19 @@ public class PrivateMessageController {
}
@GetMapping("/loadMessage")
@ApiOperation(value = "拉取消息",notes="拉取消息,一次最多拉取100条")
public Result<List<PrivateMessageVO>> loadMessage(@RequestParam Long minId){
return ResultUtils.success(privateMessageService.loadMessage(minId));
}
@PutMapping("/readed")
@ApiOperation(value = "消息已读",notes="将会话中接收的消息状态置为已读")
public Result readedMessage(@RequestParam Long friendId){
privateMessageService.readedMessage(friendId);
return ResultUtils.success();
}
@GetMapping("/history")
@ApiOperation(value = "查询聊天记录",notes="查询聊天记录")
public Result<List<PrivateMessageVO>> recallMessage(@NotNull(message = "好友id不能为空") @RequestParam Long friendId,

8
im-platform/src/main/java/com/bx/implatform/controller/UserController.java

@ -28,12 +28,6 @@ public class UserController {
private IUserService userService;
@GetMapping("/online")
@ApiOperation(value = "判断用户是否在线",notes="返回在线的用户id集合")
public Result checkOnline(@NotEmpty @RequestParam("userIds") String userIds){
List<Long> onlineIds = userService.checkOnline(userIds);
return ResultUtils.success(onlineIds);
}
@GetMapping("/terminal/online")
@ApiOperation(value = "判断用户哪个终端在线",notes="返回在线的用户id的终端集合")
@ -54,7 +48,7 @@ public class UserController {
@GetMapping("/find/{id}")
@ApiOperation(value = "查找用户",notes="根据id查找用户")
public Result findById(@NotEmpty @PathVariable("id") Long id){
public Result<UserVO> findById(@NotEmpty @PathVariable("id") Long id){
return ResultUtils.success(userService.findUserById(id));
}

12
im-platform/src/main/java/com/bx/implatform/enums/FileType.java

@ -1,5 +1,8 @@
package com.bx.implatform.enums;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum FileType {
FILE(0,"文件"),
@ -13,15 +16,6 @@ public enum FileType {
private final String desc;
FileType(Integer index, String desc) {
this.code =index;
this.desc=desc;
}
public String description() {
return desc;
}
public Integer code(){
return this.code;

17
im-platform/src/main/java/com/bx/implatform/enums/MessageStatus.java

@ -1,24 +1,19 @@
package com.bx.implatform.enums;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum MessageStatus {
UNREAD(0,"未读"),
ALREADY_READ(1,"已读"),
RECALL(2,"已撤回");
UNSEND(0,"未送达"),
SENDED(1,"送达"),
RECALL(2,"撤回"),
READED(3,"已读");
private final Integer code;
private final String desc;
MessageStatus(Integer index, String desc) {
this.code =index;
this.desc=desc;
}
public String description() {
return desc;
}
public Integer code(){
return this.code;

11
im-platform/src/main/java/com/bx/implatform/enums/MessageType.java

@ -1,6 +1,8 @@
package com.bx.implatform.enums;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum MessageType {
TEXT(0,"文字"),
@ -9,6 +11,7 @@ public enum MessageType {
AUDIO(3,"音频"),
VIDEO(4,"视频"),
RECALL(10,"撤回"),
READED(11, "已读"),
RTC_CALL(101,"呼叫"),
RTC_ACCEPT(102,"接受"),
@ -22,14 +25,6 @@ public enum MessageType {
private final String desc;
MessageType(Integer index, String desc) {
this.code =index;
this.desc=desc;
}
public String description() {
return desc;
}
public Integer code(){
return this.code;

23
im-platform/src/main/java/com/bx/implatform/enums/ResultCode.java

@ -1,5 +1,8 @@
package com.bx.implatform.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 响应码枚举
*
@ -7,6 +10,8 @@ package com.bx.implatform.enums;
* @date 2020/10/19
*
**/
@Getter
@AllArgsConstructor
public enum ResultCode {
SUCCESS(200,"成功"),
NO_LOGIN(400,"未登录"),
@ -21,24 +26,6 @@ public enum ResultCode {
private int code;
private String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}

1
im-platform/src/main/java/com/bx/implatform/listener/GroupMessageListener.java

@ -22,6 +22,7 @@ public class GroupMessageListener implements MessageListener<GroupMessageVO> {
@Override
public void process(IMSendResult<GroupMessageVO> result){
GroupMessageVO messageInfo = result.getData();
// todo 删除
// 保存该用户已拉取的最大消息id
if(result.getCode().equals(IMSendCode.SUCCESS.code())) {
String key = String.join(":",RedisKey.IM_GROUP_READED_POSITION,messageInfo.getGroupId().toString(),result.getReceiver().getId().toString());

4
im-platform/src/main/java/com/bx/implatform/listener/PrivateMessageListener.java

@ -29,8 +29,8 @@ public class PrivateMessageListener implements MessageListener<PrivateMessageVO>
if(result.getCode().equals(IMSendCode.SUCCESS.code())){
UpdateWrapper<PrivateMessage> updateWrapper = new UpdateWrapper<>();
updateWrapper.lambda().eq(PrivateMessage::getId,messageInfo.getId())
.eq(PrivateMessage::getStatus, MessageStatus.UNREAD.code())
.set(PrivateMessage::getStatus, MessageStatus.ALREADY_READ.code());
.eq(PrivateMessage::getStatus, MessageStatus.UNSEND.code())
.set(PrivateMessage::getStatus, MessageStatus.SENDED.code());
privateMessageService.update(updateWrapper);
log.info("消息已读,消息id:{},发送者:{},接收者:{},终端:{}",messageInfo.getId(),result.getSender().getId(),result.getReceiver().getId(),result.getReceiver().getTerminal());
}

4
im-platform/src/main/java/com/bx/implatform/service/IGroupMessageService.java

@ -17,5 +17,9 @@ public interface IGroupMessageService extends IService<GroupMessage> {
void pullUnreadMessage();
List<GroupMessageVO> loadMessage(Long minId);
void readedMessage(Long groupId);
List<GroupMessageVO> findHistoryMessage(Long groupId, Long page, Long size);
}

3
im-platform/src/main/java/com/bx/implatform/service/IPrivateMessageService.java

@ -18,4 +18,7 @@ public interface IPrivateMessageService extends IService<PrivateMessage> {
void pullUnreadMessage();
List<PrivateMessageVO> loadMessage(Long minId);
void readedMessage(Long friendId);
}

2
im-platform/src/main/java/com/bx/implatform/service/IUserService.java

@ -30,8 +30,6 @@ public interface IUserService extends IService<User> {
List<UserVO> findUserByName(String name);
List<Long> checkOnline(String userIds);
List<OnlineTerminalVO> getOnlineTerminals(String userIds);

2
im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java

@ -79,7 +79,7 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
@Override
public void delFriend(Long friendId) {
long userId = SessionContext.getSession().getUserId();
// 互相解除好友关系
// 互相解除好友关系,走代理清理缓存
FriendServiceImpl proxy = (FriendServiceImpl)AopContext.currentProxy();
proxy.unbindFriend(userId,friendId);
proxy.unbindFriend(friendId,userId);

184
im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java

@ -1,11 +1,14 @@
package com.bx.implatform.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.imclient.IMClient;
import com.bx.imcommon.contant.IMConstant;
import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.util.DateTimeUtils;
import com.bx.implatform.vo.GroupMessageVO;
import com.bx.imcommon.model.IMGroupMessage;
import com.bx.imcommon.model.IMUserInfo;
@ -26,6 +29,7 @@ import com.bx.implatform.session.UserSession;
import com.bx.implatform.util.BeanUtils;
import com.bx.implatform.dto.GroupMessageDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@ -33,6 +37,7 @@ import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
@Slf4j
@ -43,12 +48,12 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
@Autowired
private IGroupMemberService groupMemberService;
@Autowired
private RedisTemplate<String,Object> redisTemplate;
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private IMClient imClient;
/**
* 发送群聊消息(与mysql所有交换都要进行缓存)
* 发送群聊消息(高并发接口查询mysql接口都要进行缓存)
*
* @param dto 群聊消息
* @return 群聊id
@ -57,16 +62,16 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
public Long sendMessage(GroupMessageDTO dto) {
UserSession session = SessionContext.getSession();
Group group = groupService.getById(dto.getGroupId());
if(group == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群聊不存在");
if (group == null) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "群聊不存在");
}
if(group.getDeleted()){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"群聊已解散");
if (group.getDeleted()) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "群聊已解散");
}
// 判断是否在群里
List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId());
if(!userIds.contains(session.getUserId())){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您已不在群聊里面,无法发送消息");
if (!userIds.contains(session.getUserId())) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "您已不在群聊里面,无法发送消息");
}
// 保存消息
GroupMessage msg = BeanUtils.copyProperties(dto, GroupMessage.class);
@ -74,21 +79,19 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
msg.setSendTime(new Date());
this.save(msg);
// 不用发给自己
userIds = userIds.stream().filter(id->!session.getUserId().equals(id)).collect(Collectors.toList());
userIds = userIds.stream().filter(id -> !session.getUserId().equals(id)).collect(Collectors.toList());
// 群发
GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class);
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvIds(userIds);
sendMessage.setData(msgInfo);
imClient.sendGroupMessage(sendMessage);
log.info("发送群聊消息,发送id:{},群聊id:{},内容:{}",session.getUserId(),dto.getGroupId(),dto.getContent());
log.info("发送群聊消息,发送id:{},群聊id:{},内容:{}", session.getUserId(), dto.getGroupId(), dto.getContent());
return msg.getId();
}
/**
* 撤回消息
*
@ -98,19 +101,19 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
public void recallMessage(Long id) {
UserSession session = SessionContext.getSession();
GroupMessage msg = this.getById(id);
if(msg == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"消息不存在");
if (msg == null) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "消息不存在");
}
if(!msg.getSendId().equals(session.getUserId())){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"这条消息不是由您发送,无法撤回");
if (!msg.getSendId().equals(session.getUserId())) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "这条消息不是由您发送,无法撤回");
}
if(System.currentTimeMillis() - msg.getSendTime().getTime() > IMConstant.ALLOW_RECALL_SECOND * 1000){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"消息已发送超过5分钟,无法撤回");
if (System.currentTimeMillis() - msg.getSendTime().getTime() > IMConstant.ALLOW_RECALL_SECOND * 1000) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "消息已发送超过5分钟,无法撤回");
}
// 判断是否在群里
GroupMember member = groupMemberService.findByGroupAndUserId(msg.getGroupId(),session.getUserId());
if(member == null){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您已不在群聊里面,无法撤回消息");
GroupMember member = groupMemberService.findByGroupAndUserId(msg.getGroupId(), session.getUserId());
if (member == null) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "您已不在群聊里面,无法撤回消息");
}
// 修改数据库
msg.setStatus(MessageStatus.RECALL.code());
@ -118,15 +121,15 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
// 群发
List<Long> userIds = groupMemberService.findUserIdsByGroupId(msg.getGroupId());
// 不用发给自己
userIds = userIds.stream().filter(uid->!session.getUserId().equals(uid)).collect(Collectors.toList());
userIds = userIds.stream().filter(uid -> !session.getUserId().equals(uid)).collect(Collectors.toList());
GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class);
msgInfo.setType(MessageType.RECALL.code());
String content = String.format("'%s'撤回了一条消息",member.getAliasName());
String content = String.format("'%s'撤回了一条消息", member.getAliasName());
msgInfo.setContent(content);
msgInfo.setSendTime(new Date());
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvIds(userIds);
sendMessage.setData(msgInfo);
sendMessage.setSendResult(false);
@ -139,40 +142,39 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
sendMessage.setRecvIds(Collections.emptyList());
sendMessage.setRecvTerminals(Collections.emptyList());
imClient.sendGroupMessage(sendMessage);
log.info("撤回群聊消息,发送id:{},群聊id:{},内容:{}",session.getUserId(),msg.getGroupId(),msg.getContent());
log.info("撤回群聊消息,发送id:{},群聊id:{},内容:{}", session.getUserId(), msg.getGroupId(), msg.getContent());
}
/**
* 异步拉取群聊消息通过websocket异步推送
*
*/
@Override
public void pullUnreadMessage() {
UserSession session = SessionContext.getSession();
List<GroupMember> members = groupMemberService.findByUserId(session.getUserId());
for(GroupMember member:members){
for (GroupMember member : members) {
// 获取群聊已读的最大消息id,只推送未读消息
String key = String.join(":",RedisKey.IM_GROUP_READED_POSITION,member.getGroupId().toString(),session.getUserId().toString());
Integer maxReadedId = (Integer)redisTemplate.opsForValue().get(key);
String key = String.join(":", RedisKey.IM_GROUP_READED_POSITION, member.getGroupId().toString(), session.getUserId().toString());
Integer maxReadedId = (Integer) redisTemplate.opsForValue().get(key);
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.eq(GroupMessage::getGroupId,member.getGroupId())
.gt(GroupMessage::getSendTime,member.getCreatedTime())
wrapper.eq(GroupMessage::getGroupId, member.getGroupId())
.gt(GroupMessage::getSendTime, member.getCreatedTime())
.ne(GroupMessage::getSendId, session.getUserId())
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code());
if(maxReadedId!=null){
wrapper.gt(GroupMessage::getId,maxReadedId);
if (maxReadedId != null) {
wrapper.gt(GroupMessage::getId, maxReadedId);
}
wrapper.last("limit 100");
List<GroupMessage> messages = this.list(wrapper);
if(messages.isEmpty()){
if (messages.isEmpty()) {
continue;
}
// 推送
for (GroupMessage message:messages ){
for (GroupMessage message : messages) {
GroupMessageVO msgInfo = BeanUtils.copyProperties(message, GroupMessageVO.class);
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
// 只推给自己当前终端
sendMessage.setRecvIds(Collections.singletonList(session.getUserId()));
sendMessage.setRecvTerminals(Collections.singletonList(session.getTerminal()));
@ -180,8 +182,88 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
imClient.sendGroupMessage(sendMessage);
}
// 发送消息
log.info("拉取未读群聊消息,用户id:{},群聊id:{},数量:{}",session.getUserId(),member.getGroupId(),messages.size());
log.info("拉取未读群聊消息,用户id:{},群聊id:{},数量:{}", session.getUserId(), member.getGroupId(), messages.size());
}
}
/**
* 拉取消息只能拉取最近1个月的消息一次拉取100条
*
* @param minId 消息起始id
* @return 聊天消息列表
*/
@Override
public List<GroupMessageVO> loadMessage(Long minId) {
UserSession session = SessionContext.getSession();
List<GroupMember> members = groupMemberService.findByUserId(session.getUserId());
List<Long> ids = members.stream().map(GroupMember::getGroupId).collect(Collectors.toList());
// 只能拉取最近1个月的
Date minDate = DateTimeUtils.addMonths(new Date(), -1);
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.gt(GroupMessage::getId, minId)
.gt(GroupMessage::getSendTime, minDate)
.in(GroupMessage::getGroupId, ids)
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code())
.orderByAsc(GroupMessage::getId)
.last("limit 100");
List<GroupMessage> messages = this.list(wrapper);
// 转成vo
List<GroupMessageVO> vos = messages.stream().map(m -> BeanUtils.copyProperties(m, GroupMessageVO.class)).collect(Collectors.toList());
// 消息状态,数据库没有存群聊的消息状态,需要从redis取
List<String> keys = ids.stream()
.map(id -> String.join(":", RedisKey.IM_GROUP_READED_POSITION, id.toString(), session.getUserId().toString()))
.collect(Collectors.toList());
List<Object> sendPos = redisTemplate.opsForValue().multiGet(keys);
int idx = 0;
for (Long id : ids) {
Object o = sendPos.get(idx);
Integer sendMaxId = Objects.isNull(o) ? -1 : (Integer) o;
vos.stream().filter(vo -> vo.getGroupId().equals(id)).forEach(vo -> {
if (vo.getId() <= sendMaxId) {
// 已读
vo.setStatus(MessageStatus.READED.code());
} else {
// 未推送
vo.setStatus(MessageStatus.UNSEND.code());
}
});
idx++;
}
return vos;
}
/**
* 消息已读,同步其他终端清空未读数量
*
* @param groupId 群聊
*/
@Override
public void readedMessage(Long groupId) {
UserSession session = SessionContext.getSession();
// 推送消息给自己的其他终端
GroupMessageVO msgInfo = new GroupMessageVO();
msgInfo.setType(MessageType.READED.code());
msgInfo.setSendTime(new Date());
msgInfo.setSendId(session.getUserId());
msgInfo.setGroupId(groupId);
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setSendToSelf(true);
sendMessage.setData(msgInfo);
sendMessage.setSendResult(false);
imClient.sendGroupMessage(sendMessage);
// 记录已读位置
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.eq(GroupMessage::getGroupId, groupId)
.orderByDesc(GroupMessage::getId)
.last("limit 1")
.select(GroupMessage::getId);
GroupMessage message = this.getOne(wrapper);
String key = StrUtil.join(":",RedisKey.IM_GROUP_READED_POSITION,groupId,session.getUserId());
redisTemplate.opsForValue().set(key, message.getId());
}
@ -189,32 +271,32 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
* 拉取历史聊天记录
*
* @param groupId 群聊id
* @param page 页码
* @param size 页码大小
* @param page 页码
* @param size 页码大小
* @return 聊天记录列表
*/
@Override
public List<GroupMessageVO> findHistoryMessage(Long groupId, Long page, Long size) {
page = page > 0 ? page:1;
size = size > 0 ? size:10;
page = page > 0 ? page : 1;
size = size > 0 ? size : 10;
Long userId = SessionContext.getSession().getUserId();
long stIdx = (page-1)* size;
long stIdx = (page - 1) * size;
// 群聊成员信息
GroupMember member = groupMemberService.findByGroupAndUserId(groupId,userId);
if(member == null || member.getQuit()){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"您已不在群聊中");
GroupMember member = groupMemberService.findByGroupAndUserId(groupId, userId);
if (member == null || member.getQuit()) {
throw new GlobalException(ResultCode.PROGRAM_ERROR, "您已不在群聊中");
}
// 查询聊天记录,只查询加入群聊时间之后的消息
QueryWrapper<GroupMessage> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(GroupMessage::getGroupId,groupId)
.gt(GroupMessage::getSendTime,member.getCreatedTime())
wrapper.lambda().eq(GroupMessage::getGroupId, groupId)
.gt(GroupMessage::getSendTime, member.getCreatedTime())
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code())
.orderByDesc(GroupMessage::getId)
.last("limit "+stIdx + ","+size);
.last("limit " + stIdx + "," + size);
List<GroupMessage> messages = this.list(wrapper);
List<GroupMessageVO> messageInfos = messages.stream().map(m->BeanUtils.copyProperties(m, GroupMessageVO.class)).collect(Collectors.toList());
log.info("拉取群聊记录,用户id:{},群聊id:{},数量:{}",userId,groupId,messageInfos.size());
List<GroupMessageVO> messageInfos = messages.stream().map(m -> BeanUtils.copyProperties(m, GroupMessageVO.class)).collect(Collectors.toList());
log.info("拉取群聊记录,用户id:{},群聊id:{},数量:{}", userId, groupId, messageInfos.size());
return messageInfos;
}

102
im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java

@ -2,6 +2,7 @@ package com.bx.implatform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.imclient.IMClient;
@ -9,6 +10,7 @@ import com.bx.imcommon.contant.IMConstant;
import com.bx.imcommon.model.IMPrivateMessage;
import com.bx.imcommon.model.IMUserInfo;
import com.bx.implatform.entity.Friend;
import com.bx.implatform.util.DateTimeUtils;
import com.bx.implatform.vo.PrivateMessageVO;
import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.enums.MessageStatus;
@ -25,6 +27,7 @@ import com.bx.implatform.dto.PrivateMessageDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.Date;
@ -40,8 +43,9 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
@Autowired
private IMClient imClient;
/**
* 发送私聊消息
* 发送私聊消息(高并发接口查询mysql接口都要进行缓存)
*
* @param dto 私聊消息
* @return 消息id
@ -56,13 +60,13 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
// 保存消息
PrivateMessage msg = BeanUtils.copyProperties(dto, PrivateMessage.class);
msg.setSendId(session.getUserId());
msg.setStatus(MessageStatus.UNREAD.code());
msg.setStatus(MessageStatus.UNSEND.code());
msg.setSendTime(new Date());
this.save(msg);
// 推送消息
PrivateMessageVO msgInfo = BeanUtils.copyProperties(msg, PrivateMessageVO.class);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvId(msgInfo.getRecvId());
sendMessage.setSendToSelf(true);
sendMessage.setData(msgInfo);
@ -99,7 +103,7 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
msgInfo.setContent("对方撤回了一条消息");
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvId(msgInfo.getRecvId());
sendMessage.setSendToSelf(false);
sendMessage.setData(msgInfo);
@ -147,7 +151,6 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
/**
* 异步拉取私聊消息通过websocket异步推送
*
*/
@Override
public void pullUnreadMessage() {
@ -158,22 +161,22 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
}
List<Friend> friends = friendService.findFriendByUserId(session.getUserId());
if(friends.isEmpty()){
if (friends.isEmpty()) {
return;
}
List<Long> friendIds = friends.stream().map(Friend::getFriendId).collect(Collectors.toList());
// 获取当前用户所有未读消息
LambdaQueryWrapper<PrivateMessage> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.eq(PrivateMessage::getRecvId, session.getUserId())
.eq(PrivateMessage::getStatus, MessageStatus.UNREAD)
.in(PrivateMessage::getSendId,friendIds);
.eq(PrivateMessage::getStatus, MessageStatus.UNSEND)
.in(PrivateMessage::getSendId, friendIds);
List<PrivateMessage> messages = this.list(queryWrapper);
// 上传至redis,等待推送
for(PrivateMessage message:messages){
for (PrivateMessage message : messages) {
PrivateMessageVO msgInfo = BeanUtils.copyProperties(message, PrivateMessageVO.class);
// 推送消息
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(),session.getTerminal()));
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvId(session.getUserId());
sendMessage.setRecvTerminals(Collections.singletonList(session.getTerminal()));
sendMessage.setSendToSelf(false);
@ -183,4 +186,83 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
log.info("拉取未读私聊消息,用户id:{},数量:{}", session.getUserId(), messages.size());
}
/**
* 拉取消息只能拉取最近1个月的消息一次拉取100条
*
* @param minId 消息起始id
* @return 聊天消息列表
*/
@Override
public List<PrivateMessageVO> loadMessage(Long minId) {
UserSession session = SessionContext.getSession();
List<Friend> friends = friendService.findFriendByUserId(session.getUserId());
if (friends.isEmpty()) {
return Collections.EMPTY_LIST;
}
List<Long> friendIds = friends.stream().map(Friend::getFriendId).collect(Collectors.toList());
// 获取当前用户的消息
LambdaQueryWrapper<PrivateMessage> queryWrapper = Wrappers.lambdaQuery();
// 只能拉取最近1个月的
Date minDate = DateTimeUtils.addMonths(new Date(), -1);
queryWrapper.gt(PrivateMessage::getId, minId)
.ge(PrivateMessage::getSendTime, minDate)
.ne(PrivateMessage::getStatus, MessageStatus.RECALL.code())
.and(wrap -> wrap.and(
wp -> wp.eq(PrivateMessage::getSendId, session.getUserId())
.in(PrivateMessage::getRecvId, friendIds))
.or(wp -> wp.eq(PrivateMessage::getRecvId, session.getUserId())
.in(PrivateMessage::getSendId, friendIds)))
.orderByAsc(PrivateMessage::getId)
.last("limit 100");
List<PrivateMessage> messages = this.list(queryWrapper);
// 更新发送状态
List<Long> ids = messages.stream()
.filter(m -> !m.getSendId().equals(session.getUserId()) && m.getStatus().equals(MessageStatus.UNSEND.code()))
.map(PrivateMessage::getId)
.collect(Collectors.toList());
if (!ids.isEmpty()) {
LambdaUpdateWrapper<PrivateMessage> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.in(PrivateMessage::getId, ids)
.set(PrivateMessage::getStatus, MessageStatus.SENDED.code());
this.update(updateWrapper);
}
log.info("拉取消息,用户id:{},数量:{}", session.getUserId(), messages.size());
return messages.stream().map(m -> BeanUtils.copyProperties(m, PrivateMessageVO.class)).collect(Collectors.toList());
}
/**
* 消息已读,将整个会话的消息都置为已读状态
*
* @param friendId 好友id
*/
@Transactional
@Override
public void readedMessage(Long friendId) {
UserSession session = SessionContext.getSession();
// 推送消息
PrivateMessageVO msgInfo = new PrivateMessageVO();
msgInfo.setType(MessageType.READED.code());
msgInfo.setSendTime(new Date());
msgInfo.setSendId(session.getUserId());
msgInfo.setRecvId(friendId);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal()));
sendMessage.setRecvId(friendId);
sendMessage.setSendToSelf(true);
sendMessage.setData(msgInfo);
sendMessage.setSendResult(false);
imClient.sendPrivateMessage(sendMessage);
// 修改消息状态为已读
LambdaUpdateWrapper<PrivateMessage> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.eq(PrivateMessage::getSendId, friendId)
.eq(PrivateMessage::getRecvId, session.getUserId())
.eq(PrivateMessage::getStatus, MessageStatus.SENDED.code())
.set(PrivateMessage::getStatus, MessageStatus.READED.code());
this.update(updateWrapper);
log.info("消息已读,接收方id:{},发送方id:{}", session.getUserId(), friendId);
}
}

12
im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java

@ -246,18 +246,6 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IU
}).collect(Collectors.toList());
}
/**
* 判断用户是否在线返回在线的用户id列表
*
* @param userIds 用户id多个用,分割
* @return 在线用户id列表
*/
@Override
public List<Long> checkOnline(String userIds) {
List<Long> userIdList = Arrays.stream(userIds.split(","))
.map(Long::parseLong).collect(Collectors.toList());
return imClient.getOnlineUser(userIdList);
}
/**

16
im-platform/src/main/java/com/bx/implatform/service/thirdparty/FileService.java

@ -19,10 +19,12 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import java.io.IOException;
/*
/**
* todo 通过校验文件MD5实现重复文件秒传
* 文件上传服务
* @Author Blue
* @Date 2022/10/28
*
*/
@Slf4j
@Service
@ -87,11 +89,13 @@ public class FileService {
throw new GlobalException(ResultCode.PROGRAM_ERROR,"图片上传失败");
}
vo.setOriginUrl(generUrl(FileType.IMAGE,fileName));
// 上传缩略图
byte[] imageByte = ImageUtil.compressForScale(file.getBytes(),100);
fileName = minioUtil.upload(bucketName,imagePath,file.getOriginalFilename(),imageByte,file.getContentType());
if(StringUtils.isEmpty(fileName)){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"图片上传失败");
// 大于30K的文件需上传缩略图
if(file.getSize() > 30 * 1024){
byte[] imageByte = ImageUtil.compressForScale(file.getBytes(),30);
fileName = minioUtil.upload(bucketName,imagePath,file.getOriginalFilename(),imageByte,file.getContentType());
if(StringUtils.isEmpty(fileName)){
throw new GlobalException(ResultCode.PROGRAM_ERROR,"图片上传失败");
}
}
vo.setThumbUrl(generUrl(FileType.IMAGE,fileName));
log.info("文件图片成功,用户id:{},url:{}",userId,vo.getOriginUrl());

5
im-platform/src/main/java/com/bx/implatform/util/MinioUtil.java

@ -106,7 +106,10 @@ public class MinioUtil {
if (StringUtils.isBlank(originalFilename)){
throw new RuntimeException();
}
String fileName = System.currentTimeMillis() + originalFilename.substring(originalFilename.lastIndexOf("."));
String fileName = System.currentTimeMillis()+"";
if(originalFilename.lastIndexOf(".") >= 0){
fileName +=originalFilename.substring(originalFilename.lastIndexOf("."));
}
String objectName = DateTimeUtils.getFormatDate(new Date(),DateTimeUtils.PARTDATEFORMAT)+ "/" + fileName;
try {
PutObjectArgs objectArgs = PutObjectArgs.builder().bucket(bucketName).object(path+"/" +objectName)

28
im-platform/src/main/java/com/bx/implatform/vo/GroupMessageVO.java

@ -2,6 +2,7 @@ package com.bx.implatform.vo;
import com.bx.imcommon.serializer.DateToLongSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
@ -9,34 +10,25 @@ import java.util.Date;
@Data
public class GroupMessageVO {
/*
* 消息id
*/
@ApiModelProperty(value = "消息id")
private Long id;
/*
* 群聊id
*/
@ApiModelProperty(value = "群聊id")
private Long groupId;
/*
* 发送者id
*/
@ApiModelProperty(value = " 发送者id")
private Long sendId;
/*
* 消息内容
*/
@ApiModelProperty(value = "消息内容")
private String content;
/*
* 消息内容类型 具体枚举值由应用层定义
*/
@ApiModelProperty(value = "消息内容类型 具体枚举值由应用层定义")
private Integer type;
/**
* 发送时间
*/
@ApiModelProperty(value = " 状态")
private Integer status;
@ApiModelProperty(value = "发送时间")
@JsonSerialize(using = DateToLongSerializer.class)
private Date sendTime;
}

30
im-platform/src/main/java/com/bx/implatform/vo/PrivateMessageVO.java

@ -2,41 +2,35 @@ package com.bx.implatform.vo;
import com.bx.imcommon.serializer.DateToLongSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
@Data
@ApiModel("私聊消息VO")
public class PrivateMessageVO {
/*
* 消息id
*/
@ApiModelProperty(value = " 消息id")
private long id;
/*
* 发送者id
*/
@ApiModelProperty(value = " 发送者id")
private Long sendId;
/*
* 接收者id
*/
@ApiModelProperty(value = " 接收者id")
private Long recvId;
/*
* 发送内容
*/
@ApiModelProperty(value = " 发送内容")
private String content;
/*
* 消息内容类型 IMCmdType
*/
@ApiModelProperty(value = "消息内容类型 IMCmdType")
private Integer type;
/**
* 发送时间
*/
@ApiModelProperty(value = " 状态")
private Integer status;
@ApiModelProperty(value = " 发送时间")
@JsonSerialize(using = DateToLongSerializer.class)
private Date sendTime;
}

2
im-server/src/main/java/com/bx/imserver/netty/tcp/TcpSocketServer.java

@ -59,7 +59,7 @@ public class TcpSocketServer implements IMServer {
protected void initChannel(Channel ch) throws Exception {
// 获取职责链
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
pipeline.addLast(new IdleStateHandler(120, 0, 0, TimeUnit.SECONDS));
pipeline.addLast("encode",new MessageProtocolEncoder());
pipeline.addLast("decode",new MessageProtocolDecoder());
pipeline.addLast("handler", new IMChannelHandler());

2
im-server/src/main/java/com/bx/imserver/netty/ws/WebSocketServer.java

@ -63,7 +63,7 @@ public class WebSocketServer implements IMServer {
protected void initChannel(Channel ch) throws Exception {
// 获取职责链
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS));
pipeline.addLast(new IdleStateHandler(120, 0, 0, TimeUnit.SECONDS));
pipeline.addLast("http-codec", new HttpServerCodec());
pipeline.addLast("aggregator", new HttpObjectAggregator(65535));
pipeline.addLast("http-chunked", new ChunkedWriteHandler());

2
im-server/src/main/java/com/bx/imserver/task/PullUnreadGroupMessageTask.java

@ -23,7 +23,7 @@ public class PullUnreadGroupMessageTask extends AbstractPullMessageTask {
@Override
public void pullMessage() {
// 从redis拉取未读消息
String key = String.join(":", IMRedisKey.IM_UNREAD_GROUP_QUEUE,IMServerGroup.serverId+"");
String key = String.join(":", IMRedisKey.IM_MESSAGE_GROUP_QUEUE,IMServerGroup.serverId+"");
JSONObject jsonObject = (JSONObject)redisTemplate.opsForList().leftPop(key,10, TimeUnit.SECONDS);
if(jsonObject != null){
IMRecvInfo recvInfo = jsonObject.toJavaObject(IMRecvInfo.class);

2
im-server/src/main/java/com/bx/imserver/task/PullUnreadPrivateMessageTask.java

@ -25,7 +25,7 @@ public class PullUnreadPrivateMessageTask extends AbstractPullMessageTask {
@Override
public void pullMessage() {
// 从redis拉取未读消息
String key = String.join(":", IMRedisKey.IM_UNREAD_PRIVATE_QUEUE ,IMServerGroup.serverId+"");
String key = String.join(":", IMRedisKey.IM_MESSAGE_PRIVATE_QUEUE,IMServerGroup.serverId+"");
JSONObject jsonObject = (JSONObject)redisTemplate.opsForList().leftPop(key,10, TimeUnit.SECONDS);
if(jsonObject!=null){
IMRecvInfo recvInfo = jsonObject.toJavaObject(IMRecvInfo.class);

3
im-ui/src/api/emotion.js

@ -15,6 +15,9 @@ let transform = (content) => {
let textToImg = (emoText) => {
let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word);
if(idx==-1){
return "";
}
let url = require(`@/assets/emoji/${idx}.gif`);
return `<img src="${url}" style="width:40px;height:40px;vertical-align:bottom;"/>`
}

12
im-ui/src/api/enums.js

@ -6,6 +6,7 @@ const MESSAGE_TYPE = {
AUDIO:3,
VIDEO:4,
RECALL:10,
READED:11,
TIP_TIME:20,
RTC_CALL: 101,
RTC_ACCEPT: 102,
@ -27,8 +28,17 @@ const TERMINAL_TYPE = {
APP: 1
}
const MESSAGE_STATUS = {
UNSEND: 0,
SENDED: 1,
RECALL:2,
READED:3
}
export {
MESSAGE_TYPE,
USER_STATE,
TERMINAL_TYPE
TERMINAL_TYPE,
MESSAGE_STATUS
}

252
im-ui/src/components/chat/ChatBox.vue

@ -2,17 +2,19 @@
<el-container class="chat-box">
<el-header height="60px">
<span>{{title}}</span>
<span title="群聊信息" v-show="this.chat.type=='GROUP'" class="btn-side el-icon-more" @click="showSide=!showSide"></span>
<span title="群聊信息" v-show="this.chat.type=='GROUP'" class="btn-side el-icon-more"
@click="showSide=!showSide"></span>
</el-header>
<el-main style="padding: 0;">
<el-container>
<el-container class="content-box">
<el-main class="im-chat-main" id="chatScrollBox">
<el-main class="im-chat-main" id="chatScrollBox" @scroll="handleScroll">
<div class="im-chat-box">
<ul>
<li v-for="(msgInfo,idx) in chat.messages" :key="idx">
<chat-message-item :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
:msgInfo="msgInfo" @delete="deleteMessage" @recall="recallMessage">
<chat-message-item v-show="idx>=showMinIdx" :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)"
:showName="showName(msgInfo)" :msgInfo="msgInfo" @delete="deleteMessage"
@recall="recallMessage">
</chat-message-item>
</li>
</ul>
@ -20,31 +22,44 @@
</el-main>
<el-footer height="240px" class="im-chat-footer">
<div class="chat-tool-bar">
<div title="表情" class="icon iconfont icon-biaoqing" ref="emotion" @click="switchEmotionBox()">
<div title="表情" class="icon iconfont icon-biaoqing" ref="emotion"
@click="switchEmotionBox()">
</div>
<div title="发送图片">
<file-upload :action="imageAction" :maxSize="5*1024*1024" :fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp','image/gif']"
@before="handleImageBefore" @success="handleImageSuccess" @fail="handleImageFail">
<file-upload :action="imageAction" :maxSize="5*1024*1024"
:fileTypes="['image/jpeg', 'image/png', 'image/jpg', 'image/webp','image/gif']"
@before="handleImageBefore" @success="handleImageSuccess" @fail="handleImageFail">
<i class="el-icon-picture-outline"></i>
</file-upload>
</div>
<div title="发送文件">
<file-upload :action="fileAction" :maxSize="10*1024*1024" @before="handleFileBefore" @success="handleFileSuccess"
@fail="handleFileFail">
<file-upload :action="fileAction" :maxSize="10*1024*1024" @before="handleFileBefore"
@success="handleFileSuccess" @fail="handleFileFail">
<i class="el-icon-wallet"></i>
</file-upload>
</div>
<div title="发送语音" class="el-icon-microphone" @click="showVoiceBox()">
</div>
<div title="视频聊天" v-show="chat.type=='PRIVATE'" class="el-icon-phone-outline" @click="showVideoBox()">
<div title="视频聊天" v-show="chat.type=='PRIVATE'" class="el-icon-phone-outline"
@click="showVideoBox()">
</div>
<div title="聊天记录" class="el-icon-chat-dot-round" @click="showHistoryBox()"></div>
</div>
<textarea v-model="sendText" ref="sendBox" class="send-text-area"
:disabled="lockMessage" @keydown.enter="sendTextMessage()"
placeholder="聊点什么吧~"></textarea>
<div class="im-chat-send">
<el-button type="primary" size="small" @click="sendTextMessage()">发送</el-button>
<div class="send-content-area">
<textarea v-show="!sendImageUrl" v-model="sendText" ref="sendBox" class="send-text-area"
:disabled="lockMessage" @keydown.enter="sendTextMessage()" @paste="handlePaste"
placeholder="温馨提示:可以粘贴截图到这里了哦~"></textarea>
<div v-show="sendImageUrl" class="send-image-area">
<div class="send-image-box">
<img class="send-image" :src="sendImageUrl" />
<span class="send-image-close el-icon-close" title="删除"
@click="removeSendImage()"></span>
</div>
</div>
<div class="send-btn-area">
<el-button type="primary" size="small" @click="handleSendMessage()">发送</el-button>
</div>
</div>
</el-footer>
</el-container>
@ -56,7 +71,8 @@
</el-main>
<emotion v-show="showEmotion" :pos="emoBoxPos" @emotion="handleEmotion"></Emotion>
<chat-voice :visible="showVoice" @close="closeVoiceBox" @send="handleSendVoice"></chat-voice>
<chat-history :visible="showHistory" :chat="chat" :friend="friend" :group="group" :groupMembers="groupMembers" @close="closeHistoryBox"></chat-history>
<chat-history :visible="showHistory" :chat="chat" :friend="friend" :group="group" :groupMembers="groupMembers"
@close="closeHistoryBox"></chat-history>
</el-container>
</template>
@ -89,6 +105,8 @@
group: {},
groupMembers: [],
sendText: "",
sendImageUrl: "",
sendImageFile: "",
showVoice: false, //
showSide: false, //
showEmotion: false, // emoji
@ -97,13 +115,34 @@
y: 0
},
showHistory: false, //
lockMessage: false //
lockMessage: false, //
showMinIdx: 0 // showMinIdx
}
},
methods: {
handleImageSuccess(res, file) {
let msgInfo = JSON.parse(JSON.stringify(file.raw.msgInfo));
msgInfo.content = JSON.stringify(res.data);
handlePaste(e) {
let txt = event.clipboardData.getData('Text')
if (typeof(txt) == 'string') {
this.sendText += txt
}
const items = (event.clipboardData || window.clipboardData).items
if (items.length) {
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
let file = items[i].getAsFile();
this.sendImageFile = file;
this.sendImageUrl = URL.createObjectURL(file);
}
}
}
},
removeSendImage() {
this.sendImageUrl = "";
this.sendImageFile = null;
},
handleImageSuccess(data, file) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo || file.raw.msgInfo));
msgInfo.content = JSON.stringify(data);
this.$http({
url: this.messageAction,
method: 'post',
@ -114,8 +153,8 @@
this.$store.commit("insertMessage", msgInfo);
})
},
handleImageFail(res, file) {
let msgInfo = JSON.parse(JSON.stringify(file.raw.msgInfo));
handleImageFail(e, file) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo || file.raw.msgInfo));
msgInfo.loadStatus = 'fail';
this.$store.commit("insertMessage", msgInfo);
},
@ -133,7 +172,8 @@
sendTime: new Date().getTime(),
selfSend: true,
type: 1,
loadStatus: "loading"
loadStatus: "loading",
status: this.$enums.MESSAGE_STATUS.UNSEND
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
@ -144,11 +184,11 @@
// file
file.msgInfo = msgInfo;
},
handleFileSuccess(res, file) {
handleFileSuccess(url, file) {
let data = {
name: file.name,
size: file.size,
url: res.data
url: url
}
let msgInfo = JSON.parse(JSON.stringify(file.raw.msgInfo));
msgInfo.content = JSON.stringify(data);
@ -162,7 +202,8 @@
this.$store.commit("insertMessage", msgInfo);
})
},
handleFileFail(res, file) {
handleFileFail(e, file) {
let msgInfo = JSON.parse(JSON.stringify(file.raw.msgInfo));
msgInfo.loadStatus = 'fail';
this.$store.commit("insertMessage", msgInfo);
@ -181,7 +222,8 @@
sendTime: new Date().getTime(),
selfSend: true,
type: 2,
loadStatus: "loading"
loadStatus: "loading",
status: this.$enums.MESSAGE_STATUS.UNSEND
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
@ -195,6 +237,18 @@
handleCloseSide() {
this.showSide = false;
},
handleScrollToTop() {
// 10
this.showMinIdx = this.showMinIdx > 10 ? this.showMinIdx - 10 : 0;
},
handleScroll(e) {
let scrollElement = e.target
let scrollTop = scrollElement.scrollTop
if (scrollTop < 30 ) { // ,
// 20
this.showMinIdx = this.showMinIdx > 20 ? this.showMinIdx - 20 : 0;
}
},
switchEmotionBox() {
this.showEmotion = !this.showEmotion;
let width = this.$refs.emotion.offsetWidth;
@ -216,7 +270,6 @@
this.showVoice = false;
},
showVideoBox() {
console.log(this.friend)
this.$store.commit("showChatPrivateVideoBox", {
friend: this.friend,
master: true
@ -240,11 +293,11 @@
method: 'post',
data: msgInfo
}).then((id) => {
this.$message.success("发送成功");
msgInfo.id = id;
msgInfo.sendTime = new Date().getTime();
msgInfo.sendId = this.$store.state.userStore.userInfo.id;
msgInfo.selfSend = true;
msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
this.$store.commit("insertMessage", msgInfo);
//
this.$refs.sendBox.focus();
@ -261,9 +314,34 @@
msgInfo.recvId = targetId;
}
},
handleSendMessage() {
if (this.sendImageFile) {
this.sendImageMessage();
} else {
this.sendTextMessage();
}
},
sendImageMessage() {
let file = this.sendImageFile;
this.handleImageBefore(this.sendImageFile);
let formData = new FormData()
formData.append('file', file.raw || file)
this.$http.post("/image/upload", formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
}).then((data) => {
this.handleImageSuccess(data, file);
}).catch((res) => {
this.handleImageSuccess(res, file);
})
this.sendImageFile = null;
this.sendImageUrl = "";
this.$nextTick(() => this.$refs.sendBox.focus());
this.scrollToBottom();
},
sendTextMessage() {
if (!this.sendText.trim()) {
this.$message.error("不能发送空白信息");
return
}
let msgInfo = {
@ -278,12 +356,12 @@
method: 'post',
data: msgInfo
}).then((id) => {
this.$message.success("发送成功");
this.sendText = "";
msgInfo.id = id;
msgInfo.sendTime = new Date().getTime();
msgInfo.sendId = this.$store.state.userStore.userInfo.id;
msgInfo.selfSend = true;
msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
this.$store.commit("insertMessage", msgInfo);
}).finally(() => {
//
@ -324,10 +402,24 @@
msgInfo = JSON.parse(JSON.stringify(msgInfo));
msgInfo.type = 10;
msgInfo.content = '你撤回了一条消息';
msgInfo.status = this.$enums.MESSAGE_STATUS.RECALL;
this.$store.commit("insertMessage", msgInfo);
})
});
},
readedMessage() {
if (this.chat.type == "GROUP") {
var url = `/message/group/readed?groupId=${this.chat.targetId}`
} else {
url = `/message/private/readed?friendId=${this.chat.targetId}`
}
this.$http({
url: url,
method: 'put'
}).then(() => {
this.$store.commit("resetUnreadCount", this.chat)
this.scrollToBottom();
})
},
loadGroup(groupId) {
this.$http({
@ -354,7 +446,6 @@
method: 'get'
}).then((friend) => {
this.friend = friend;
console.log(this.friend)
this.$store.commit("updateChatFromFriend", friend);
this.$store.commit("updateFriend", friend);
})
@ -378,7 +469,7 @@
},
scrollToBottom() {
this.$nextTick(() => {
const div = document.getElementById("chatScrollBox");
let div = document.getElementById("chatScrollBox");
div.scrollTop = div.scrollHeight;
});
}
@ -403,19 +494,29 @@
},
messageAction() {
return `/message/${this.chat.type.toLowerCase()}/send`;
},
unreadCount() {
return this.chat.unreadCount;
}
},
watch: {
chat: {
handler(newChat, oldChat) {
if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type || newChat.targetId != oldChat.targetId)) {
if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type ||
newChat.targetId != oldChat.targetId)) {
if (this.chat.type == "GROUP") {
this.loadGroup(this.chat.targetId);
} else {
this.loadFriend(this.chat.targetId);
}
//
this.scrollToBottom();
this.sendText = "";
//
this.readedMessage()
// 30
let size = this.chat.messages.length;
this.showMinIdx = size > 30 ? size - 30 : 0;
//
this.$nextTick(() => {
this.$refs.sendBox.focus();
@ -423,7 +524,19 @@
}
},
immediate: true
},
unreadCount: {
handler(newCount, oldCount) {
if (newCount > 0) {
//
this.readedMessage()
}
}
}
},
mounted() {
let div = document.getElementById("chatScrollBox");
div.addEventListener('scroll', this.handleScroll)
}
}
</script>
@ -498,22 +611,65 @@
}
}
.send-text-area {
box-sizing: border-box;
padding: 5px;
width: 100%;
flex: 1;
resize: none;
font-size: 16px;
color: black;
.send-content-area {
display: flex;
flex-direction: column;
height: 100%;
background-color: #f8f8f8 !important;
outline-color: rgba(83, 160, 231, 0.61);
}
.im-chat-send {
text-align: right;
padding: 7px;
.send-text-area {
box-sizing: border-box;
padding: 5px;
width: 100%;
flex: 1;
resize: none;
font-size: 16px;
color: black;
background-color: #f8f8f8 !important;
outline-color: rgba(83, 160, 231, 0.61);
}
.send-image-area {
text-align: left;
.send-image-box {
position: relative;
display: inline-block;
.send-image {
max-height: 190px;
border: 1px solid #ccc;
border-radius: 2%;
margin: 2px;
}
.send-image-close {
position: absolute;
padding: 3px;
right: 7px;
top: 7px;
color: white;
cursor: pointer;
font-size: 15px;
font-weight: 600;
background-color: #aaa;
border-radius: 50%;
border: 1px solid #ccc;
}
}
}
.send-btn-area {
padding: 10px;
position: absolute;
bottom: 0;
right: 0;
}
}
}
.chat-group-side-box {
@ -521,4 +677,4 @@
animation: rtl-drawer-in .3s 1ms;
}
}
</style>
</style>

60
im-ui/src/components/chat/ChatMessageItem.vue

@ -1,11 +1,13 @@
<template>
<div class="chat-msg-item">
<div class="chat-msg-tip" v-show="msgInfo.type==$enums.MESSAGE_TYPE.RECALL">{{msgInfo.content}}</div>
<div class="chat-msg-tip" v-show="msgInfo.type==$enums.MESSAGE_TYPE.TIP_TIME">{{$date.toTimeText(msgInfo.sendTime)}}</div>
<div class="chat-msg-tip" v-show="msgInfo.type==$enums.MESSAGE_TYPE.TIP_TIME">
{{$date.toTimeText(msgInfo.sendTime)}}
</div>
<div class="chat-msg-normal" v-show="msgInfo.type>=0 && msgInfo.type<10" :class="{'chat-msg-mine':mine}">
<div class="head-image">
<head-image :name="showName" :size="40" :url="headImage" :id="msgInfo.sendId"></head-image>
<head-image :name="showName" :size="40" :url="headImage" :id="msgInfo.sendId"></head-image>
</div>
<div class="chat-msg-content">
<div v-show="mode==1 && msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top">
@ -45,9 +47,13 @@
@click="handlePlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio>
</div>
<span class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status==$enums.MESSAGE_STATUS.READED">已读</span>
<span class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status!=$enums.MESSAGE_STATUS.READED">未读</span>
</div>
</div>
</div>
<right-menu v-show="menu && rightMenu.show" :pos="rightMenu.pos" :items="menuItems"
@close="rightMenu.show=false" @select="handleSelectMenu"></right-menu>
@ -65,9 +71,9 @@
RightMenu
},
props: {
mode:{
mode: {
type: Number,
default :1
default: 1
},
mine: {
type: Boolean,
@ -168,9 +174,6 @@
}
return items;
}
},
mounted() {
console.log(this.msgInfo);
}
}
</script>
@ -189,7 +192,7 @@
padding-left: 60px;
min-height: 50px;
margin-top: 10px;
.head-image {
position: absolute;
width: 40px;
@ -200,7 +203,14 @@
.chat-msg-content {
text-align: left;
.send-fail {
color: #e60c0c;
font-size: 30px;
cursor: pointer;
margin: 0 20px;
}
.chat-msg-top {
display: flex;
flex-wrap: nowrap;
@ -223,7 +233,7 @@
line-height: 30px;
margin-top: 3px;
padding: 7px;
background-color: rgb(235,235,245);
background-color: rgb(235, 235, 245);
border-radius: 10px;
color: black;
display: block;
@ -231,7 +241,8 @@
text-align: left;
white-space: pre-wrap;
word-break: break-all;
box-shadow: 2px 2px 2px #c0c0f0;
box-shadow: 2px 2px 2px #c0c0f0;
&:after {
content: "";
position: absolute;
@ -240,7 +251,7 @@
width: 0;
height: 0;
border-style: solid dashed dashed;
border-color: rgb(235,235,245) transparent transparent;
border-color: rgb(235, 235, 245) transparent transparent;
overflow: hidden;
border-width: 10px;
}
@ -262,13 +273,7 @@
border-radius: 6px;
cursor: pointer;
}
c
.send-fail {
color: #e60c0c;
font-size: 30px;
cursor: pointer;
margin: 0 20px;
}
}
.chat-msg-file {
@ -331,6 +336,18 @@ c
padding: 5px 0;
}
}
.chat-unread {
font-size: 10px;
color: #f23c0f;
font-weight: 600;
}
.chat-readed {
font-size: 10px;
color: #888;
font-weight: 600;
}
}
}
@ -367,6 +384,7 @@ c
color: #fff;
vertical-align: top;
box-shadow: 2px 2px 1px #ccc;
&:after {
left: auto;
right: -10px;

2
im-ui/src/components/common/FileUpload.vue

@ -40,7 +40,7 @@
handleSuccess(res, file) {
this.loading && this.loading.close();
if (res.code == 200) {
this.$emit("success", res, file);
this.$emit("success", res.data, file);
} else {
this.$message.error(res.message);
this.$emit("fail", res, file);

60
im-ui/src/components/common/FullImage.vue

@ -1,10 +1,11 @@
<template>
<el-dialog width="75%" top="30px" :visible.sync="visible" :before-close="handleClose" :modal="true">
<div class="full-image" v-show="visible" :before-close="handleClose" :modal="true">
<div class="mask"></div>
<div class="image-box">
<el-image :src="url" :fit="fit"></el-image>
<img :src="url"/>
</div>
</el-dialog>
<div class="close" @click="handleClose">x</div>
</div>
</template>
<script>
@ -12,7 +13,7 @@
name: "fullImage",
data() {
return {
fit: 'cover'
fit: 'contain'
}
},
methods: {
@ -31,12 +32,47 @@
}
</script>
<style>
.image-box {
display: flex;
align-items: center;
justify-content: center;
<style lang="scss">
.full-image{
position: fixed;
width: 100%;
height: 100%;
.mask{
position: fixed;
width: 100%;
height: 100%;
background: black;
opacity: 0.9;
}
.image-box {
position: relative;
width: 100%;
height: 100%;
img{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
max-height: 100%;
max-width: 100%;
}
}
.close{
position: fixed;
top: 10px;
right: 10px;
color: white;
font-size: 25px;
cursor: pointer;
}
}
</style>

158
im-ui/src/store/chatStore.js

@ -1,18 +1,36 @@
import {MESSAGE_TYPE} from "../api/enums.js"
import {
MESSAGE_TYPE,
MESSAGE_STATUS
} from "../api/enums.js"
import userStore from './userStore';
export default {
state: {
activeIndex: -1,
privateMsgMaxId: 0,
groupMsgMaxId: 0,
loadingPrivateMsg: false,
loadingGroupMsg: false,
chats: []
},
mutations: {
initChatStore(state) {
initChats(state, chatsData) {
state.chats = chatsData.chats || [];
state.privateMsgMaxId = chatsData.privateMsgMaxId || 0;
state.groupMsgMaxId = chatsData.groupMsgMaxId || 0;
// 防止图片一直处在加载中状态
state.chats.forEach((chat) => {
chat.messages.forEach((msg) => {
if (msg.loadStatus == "loading") {
msg.loadStatus = "fail"
}
})
})
},
openChat(state, chatInfo) {
let chat = null;
let activeChat = state.activeIndex>=0?state.chats[state.activeIndex]:null;
let activeChat = state.activeIndex >= 0 ? state.chats[state.activeIndex] : null;
for (let i in state.chats) {
if (state.chats[i].type == chatInfo.type &&
state.chats[i].targetId === chatInfo.targetId) {
@ -38,10 +56,10 @@ export default {
state.chats.unshift(chat);
}
// 选中会话保持不变
if(activeChat){
state.chats.forEach((chat,idx)=>{
if(activeChat.type == chat.type
&& activeChat.targetId == chat.targetId){
if (activeChat) {
state.chats.forEach((chat, idx) => {
if (activeChat.type == chat.type &&
activeChat.targetId == chat.targetId) {
state.activeIndex = idx;
}
})
@ -49,20 +67,42 @@ export default {
},
activeChat(state, idx) {
state.activeIndex = idx;
state.chats[idx].unreadCount = 0;
},
resetUnreadCount(state, chatInfo) {
for (let idx in state.chats) {
if (state.chats[idx].type == chatInfo.type &&
state.chats[idx].targetId == chatInfo.targetId) {
state.chats[idx].unreadCount = 0;
}
}
this.commit("saveToStorage");
},
readedMessage(state, friendId) {
for (let idx in state.chats) {
if (state.chats[idx].type == 'PRIVATE' &&
state.chats[idx].targetId == friendId) {
state.chats[idx].messages.forEach((m) => {
if (m.selfSend && m.status != MESSAGE_STATUS.RECALL) {
m.status = MESSAGE_STATUS.READED
}
})
}
}
this.commit("saveToStorage");
},
removeChat(state, idx) {
state.chats.splice(idx, 1);
if (state.activeIndex >= state.chats.length) {
state.activeIndex = state.chats.length - 1;
}
this.commit("saveToStorage");
},
moveTop(state,idx){
moveTop(state, idx) {
let chat = state.chats[idx];
// 放置头部
state.chats.splice(idx, 1);
state.chats.unshift(chat);
this.commit("saveToStorage");
},
removeGroupChat(state, groupId) {
for (let idx in state.chats) {
@ -72,15 +112,16 @@ export default {
}
}
},
removePrivateChat(state, userId) {
removePrivateChat(state, friendId) {
for (let idx in state.chats) {
if (state.chats[idx].type == 'PRIVATE' &&
state.chats[idx].targetId == userId) {
state.chats[idx].targetId == friendId) {
this.commit("removeChat", idx);
}
}
},
insertMessage(state, msgInfo) {
// 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
@ -93,36 +134,44 @@ export default {
}
}
// 插入新的数据
if(msgInfo.type == MESSAGE_TYPE.IMAGE ){
chat.lastContent = "[图片]";
}else if(msgInfo.type == MESSAGE_TYPE.FILE){
if (msgInfo.type == MESSAGE_TYPE.IMAGE) {
chat.lastContent = "[图片]";
} else if (msgInfo.type == MESSAGE_TYPE.FILE) {
chat.lastContent = "[文件]";
}else if(msgInfo.type == MESSAGE_TYPE.AUDIO){
} else if (msgInfo.type == MESSAGE_TYPE.AUDIO) {
chat.lastContent = "[语音]";
}else{
chat.lastContent = msgInfo.content;
} else {
chat.lastContent = msgInfo.content;
}
chat.lastSendTime = msgInfo.sendTime;
// 如果不是当前会话,未读加1
chat.unreadCount++;
if(msgInfo.selfSend){
chat.unreadCount=0;
// 未读加1
if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) {
chat.unreadCount++;
}
// 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
state.privateMsgMaxId = msgInfo.id;
}
if (msgInfo.id && type == "GROUP" && msgInfo.id > state.groupMsgMaxId) {
state.groupMsgMaxId = msgInfo.id;
}
// 如果是已存在消息,则覆盖旧的消息数据
for (let idx in chat.messages) {
if(msgInfo.id && chat.messages[idx].id == msgInfo.id){
if (msgInfo.id && chat.messages[idx].id == msgInfo.id) {
Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage");
return;
}
// 正在发送中的消息可能没有id,通过发送时间判断
if(msgInfo.selfSend && chat.messages[idx].selfSend
&& chat.messages[idx].sendTime == msgInfo.sendTime){
if (msgInfo.selfSend && chat.messages[idx].selfSend &&
chat.messages[idx].sendTime == msgInfo.sendTime) {
Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage");
return;
}
}
// 间隔大于10分钟插入时间显示
if(!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600*1000)){
if (!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600 * 1000)) {
chat.messages.push({
sendTime: msgInfo.sendTime,
type: MESSAGE_TYPE.TIP_TIME,
@ -131,9 +180,9 @@ export default {
}
// 新的消息
chat.messages.push(msgInfo);
this.commit("saveToStorage");
},
deleteMessage(state, msgInfo){
deleteMessage(state, msgInfo) {
// 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
@ -145,45 +194,78 @@ export default {
break;
}
}
for (let idx in chat.messages) {
// 已经发送成功的,根据id删除
if(chat.messages[idx].id && chat.messages[idx].id == msgInfo.id){
if (chat.messages[idx].id && chat.messages[idx].id == msgInfo.id) {
chat.messages.splice(idx, 1);
break;
}
// 正在发送中的消息可能没有id,根据发送时间删除
if(msgInfo.selfSend && chat.messages[idx].selfSend
&&chat.messages[idx].sendTime == msgInfo.sendTime){
if (msgInfo.selfSend && chat.messages[idx].selfSend &&
chat.messages[idx].sendTime == msgInfo.sendTime) {
chat.messages.splice(idx, 1);
break;
}
}
this.commit("saveToStorage");
},
updateChatFromFriend(state, friend) {
for (let i in state.chats) {
let chat = state.chats[i];
if (chat.type=='PRIVATE' && chat.targetId == friend.id) {
if (chat.type == 'PRIVATE' && chat.targetId == friend.id) {
chat.headImage = friend.headImageThumb;
chat.showName = friend.nickName;
break;
}
}
this.commit("saveToStorage");
},
updateChatFromGroup(state, group) {
for (let i in state.chats) {
let chat = state.chats[i];
if (chat.type=='GROUP' && chat.targetId == group.id) {
if (chat.type == 'GROUP' && chat.targetId == group.id) {
chat.headImage = group.headImageThumb;
chat.showName = group.remark;
break;
}
}
this.commit("saveToStorage");
},
loadingPrivateMsg(state, loadding) {
state.loadingPrivateMsg = loadding;
},
loadingGroupMsg(state, loadding) {
state.loadingGroupMsg = loadding;
},
resetChatStore(state) {
saveToStorage(state) {
let userId = userStore.state.userInfo.id;
let key = "chats-" + userId;
let chatsData = {
privateMsgMaxId: state.privateMsgMaxId,
groupMsgMaxId: state.groupMsgMaxId,
chats: state.chats
}
localStorage.setItem(key, JSON.stringify(chatsData));
},
clear(state) {
state.activeIndex = -1;
state.chats = [];
}
},
}
actions: {
loadChat(context) {
return new Promise((resolve, reject) => {
let userId = userStore.state.userInfo.id;
let key = "chats-" + userId;
let item = localStorage.getItem(key)
if (item) {
let chatsData = JSON.parse(item);
context.commit("initChats", chatsData);
}
resolve();
})
}
}
}

41
im-ui/src/store/friendStore.js

@ -1,4 +1,4 @@
import httpRequest from '../api/httpRequest.js'
import http from '../api/httpRequest.js'
import {TERMINAL_TYPE} from "../api/enums.js"
export default {
@ -9,16 +9,6 @@ export default {
timer: null
},
mutations: {
initFriendStore(state) {
httpRequest({
url: '/friend/list',
method: 'get'
}).then((friends) => {
this.commit("setFriends",friends);
this.commit("refreshOnlineStatus");
})
},
setFriends(state, friends) {
state.friends = friends;
},
@ -50,7 +40,7 @@ export default {
return;
}
state.friends.forEach((f)=>{userIds.push(f.id)});
httpRequest({
http({
url: '/user/terminal/online',
method: 'get',
params: {userIds: userIds.join(',')}
@ -89,8 +79,7 @@ export default {
}
return 0;
});
console.log(state.friends)
// 重新排序后,activeIndex指向的好友可能会变化,需要重新指定
if(state.activeIndex >=0){
state.friends.forEach((f,i)=>{
@ -99,7 +88,29 @@ export default {
}
})
}
},
clear(state) {
clearTimeout(state.timer);
state.friends = [];
state.timer = null;
state.activeIndex = -1;
}
},
actions: {
loadFriend(context) {
return new Promise((resolve, reject) => {
http({
url: '/friend/list',
method: 'GET'
}).then((friends) => {
context.commit("setFriends", friends);
context.commit("refreshOnlineStatus");
console.log("loadFriend")
resolve()
}).catch((res) => {
reject();
})
});
}
}
}

56
im-ui/src/store/groupStore.js

@ -1,4 +1,4 @@
import httpRequest from '../api/httpRequest.js'
import http from '../api/httpRequest.js'
export default {
@ -7,41 +7,53 @@ export default {
activeIndex: -1,
},
mutations: {
initGroupStore(state) {
httpRequest({
url: '/group/list',
method: 'get'
}).then((groups) => {
this.commit("setGroups",groups);
})
},
setGroups(state,groups){
setGroups(state, groups) {
state.groups = groups;
},
activeGroup(state,index){
activeGroup(state, index) {
state.activeIndex = index;
},
addGroup(state,group){
addGroup(state, group) {
state.groups.unshift(group);
},
removeGroup(state,groupId){
state.groups.forEach((g,index)=>{
if(g.id==groupId){
removeGroup(state, groupId) {
state.groups.forEach((g, index) => {
if (g.id == groupId) {
state.groups.splice(index, 1);
if(state.activeIndex >= state.groups.length){
state.activeIndex = state.groups.length-1;
if (state.activeIndex >= state.groups.length) {
state.activeIndex = state.groups.length - 1;
}
}
})
},
updateGroup(state,group){
state.groups.forEach((g,idx)=>{
if(g.id==group.id){
updateGroup(state, group) {
state.groups.forEach((g, idx) => {
if (g.id == group.id) {
// 拷贝属性
Object.assign(state.groups[idx], group);
}
})
},
clear(state){
state.groups = [];
state.activeGroup = -1;
}
},
actions: {
loadGroup(context) {
return new Promise((resolve, reject) => {
http({
url: '/group/list',
method: 'GET'
}).then((groups) => {
context.commit("setGroups", groups);
console.log("loadGroup")
resolve();
}).catch((res) => {
reject(res);
})
});
}
}
}
}

27
im-ui/src/store/index.js

@ -5,27 +5,28 @@ import friendStore from './friendStore.js';
import userStore from './userStore.js';
import groupStore from './groupStore.js';
import uiStore from './uiStore.js';
import VuexPersistence from 'vuex-persist'
const vuexLocal = new VuexPersistence({
storage: window.localStorage,
modules: ["userStore","chatStore"]
})
Vue.use(Vuex)
export default new Vuex.Store({
modules: {chatStore,friendStore,userStore,groupStore,uiStore},
state: {},
plugins: [vuexLocal.plugin],
mutations: {
initStore(state){
this.commit("initFriendStore");
this.commit("initGroupStore");
this.commit("initChatStore");
},
actions: {
load(context) {
console.log("load")
return this.dispatch("loadUser").then(() => {
const promises = [];
promises.push(this.dispatch("loadFriend"));
promises.push(this.dispatch("loadGroup"));
promises.push(this.dispatch("loadChat"));
return Promise.all(promises);
})
},
unload(context){
context.commit("clear");
}
},
strict: process.env.NODE_ENV !== 'production'
})

1
im-ui/src/store/uiStore.js

@ -20,7 +20,6 @@ export default {
},
videoAcceptor:{ // 视频呼叫选择
show: false,
friend:{}
}

28
im-ui/src/store/userStore.js

@ -1,4 +1,5 @@
import {USER_STATE} from "../api/enums.js"
import http from '../api/httpRequest.js'
export default {
@ -11,16 +12,29 @@ export default {
mutations: {
setUserInfo(state, userInfo) {
// 切换用户后,清理缓存
if(userInfo.id != state.userInfo.id){
console.log("用户切换")
this.commit("resetChatStore");
}
state.userInfo = userInfo;
state.userInfo = userInfo
},
setUserState(state, userState) {
state.state = userState;
},
clear(state){
state.userInfo = {};
state.state = USER_STATE.FREE;
}
},
actions:{
loadUser(context){
return new Promise((resolve, reject) => {
http({
url: '/user/self',
method: 'GET'
}).then((userInfo) => {
context.commit("setUserInfo",userInfo);
resolve();
}).catch((res)=>{
reject(res);
});
})
}
}
}

30
im-ui/src/view/Chat.vue

@ -6,14 +6,14 @@
<el-button slot="append" icon="el-icon-search"></el-button>
</el-input>
</div>
<el-scrollbar class="l-chat-list" >
<div class="l-chat-loadding" v-if="loading" v-loading="true" element-loading-text="消息接收中..."
element-loading-spinner="el-icon-loading" element-loading-background="#eee">
</div>
<el-scrollbar class="l-chat-list">
<div v-for="(chat,index) in chatStore.chats" :key="index">
<chat-item v-show="chat.showName.startsWith(searchText)"
:chat="chat" :index="index"
@click.native="handleActiveItem(index)"
@delete="handleDelItem(index)"
@top="handleTop(index)"
:active="index === chatStore.activeIndex"></chat-item>
<chat-item v-show="chat.showName.startsWith(searchText)" :chat="chat" :index="index"
@click.native="handleActiveItem(index)" @delete="handleDelItem(index)" @top="handleTop(index)"
:active="index === chatStore.activeIndex"></chat-item>
</div>
</el-scrollbar>
</el-aside>
@ -26,7 +26,7 @@
<script>
import ChatItem from "../components/chat/ChatItem.vue";
import ChatBox from "../components/chat/ChatBox.vue";
export default {
name: "chat",
components: {
@ -38,7 +38,7 @@
searchText: "",
messageContent: "",
group: {},
groupMembers: []
groupMembers: []
}
},
methods: {
@ -70,6 +70,9 @@
messages: []
}
return emptyChat;
},
loading(){
return this.chatStore.loadingGroupMsg || this.chatStore.loadingPrivateMsg
}
}
}
@ -90,9 +93,14 @@
line-height: 50px;
}
.l-friend-ist{
.l-chat-loadding{
height: 50px;
background-color: #eee;
}
.l-friend-ist {
flex: 1;
}
}
}
</style>
</style>

269
im-ui/src/view/Home.vue

@ -2,9 +2,9 @@
<el-container>
<el-aside width="80px" class="navi-bar">
<div class="user-head-image">
<head-image :name="$store.state.userStore.userInfo.nickName"
:url="$store.state.userStore.userInfo.headImageThumb"
:size="60" @click.native="showSettingDialog=true">
<head-image :name="$store.state.userStore.userInfo.nickName"
:url="$store.state.userStore.userInfo.headImageThumb" :size="60"
@click.native="showSettingDialog=true">
</head-image>
</div>
@ -29,14 +29,8 @@
<el-menu-item title="设置" @click="showSetting()">
<span class="el-icon-setting"></span>
</el-menu-item>
<el-menu-item title="小伙子这么帅,点点star吧~">
<a href="https://gitee.com/bluexsx/box-im" target="_blank">
<el-image style="width: 30px; height: 30px" src="https://gitee.com/favicon.ico" fit="fit">
</el-image>
</a>
</el-menu-item>
</el-menu>
<div class="exit-box" @click="handleExit()" title="退出">
<span class="el-icon-circle-close"></span>
</div>
@ -45,13 +39,16 @@
<router-view></router-view>
</el-main>
<setting :visible="showSettingDialog" @close="closeSetting()"></setting>
<user-info v-show="uiStore.userInfo.show" :pos="uiStore.userInfo.pos" :user="uiStore.userInfo.user" @close="$store.commit('closeUserInfoBox')"></user-info>
<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url" @close="$store.commit('closeFullImageBox')"></full-image>
<chat-private-video ref="privateVideo" :visible="uiStore.chatPrivateVideo.show" :friend="uiStore.chatPrivateVideo.friend"
:master="uiStore.chatPrivateVideo.master" :offer="uiStore.chatPrivateVideo.offer" @close="$store.commit('closeChatPrivateVideoBox')">
<user-info v-show="uiStore.userInfo.show" :pos="uiStore.userInfo.pos" :user="uiStore.userInfo.user"
@close="$store.commit('closeUserInfoBox')"></user-info>
<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url"
@close="$store.commit('closeFullImageBox')"></full-image>
<chat-private-video ref="privateVideo" :visible="uiStore.chatPrivateVideo.show"
:friend="uiStore.chatPrivateVideo.friend" :master="uiStore.chatPrivateVideo.master"
:offer="uiStore.chatPrivateVideo.offer" @close="$store.commit('closeChatPrivateVideoBox')">
</chat-private-video>
<chat-video-acceptor ref="videoAcceptor" v-show="uiStore.videoAcceptor.show" :friend="uiStore.videoAcceptor.friend"
@close="$store.commit('closeVideoAcceptorBox')">
<chat-video-acceptor ref="videoAcceptor" v-show="uiStore.videoAcceptor.show"
:friend="uiStore.videoAcceptor.friend" @close="$store.commit('closeVideoAcceptorBox')">
</chat-video-acceptor>
</el-container>
</template>
@ -77,84 +74,121 @@
data() {
return {
showSettingDialog: false,
lastPlayAudioTime: new Date()-1000
}
},
methods: {
init(userInfo) {
this.$store.commit("setUserInfo", userInfo);
this.$store.commit("setUserState", this.$enums.USER_STATE.FREE);
this.$store.commit("initStore");
this.$wsApi.init(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
this.$wsApi.connect();
this.$wsApi.onOpen(() => {
this.pullUnreadMessage();
});
this.$wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) {
// 线
this.$message.error("您已在其他地方登陆,将被强制下线");
setTimeout(() => {
init() {
this.$store.dispatch("load").then(() => {
// 线
this.loadPrivateMessage(this.$store.state.chatStore.privateMsgMaxId);
this.loadGroupMessage(this.$store.state.chatStore.groupMsgMaxId);
// ws
this.$wsApi.init(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
this.$wsApi.connect();
this.$wsApi.onOpen();
this.$wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) {
// 线
this.$message.error("您已在其他地方登陆,将被强制下线");
setTimeout(() => {
location.href = "/";
}, 1000)
} else if (cmd == 3) {
//
this.handlePrivateMessage(msgInfo);
} else if (cmd == 4) {
//
this.handleGroupMessage(msgInfo);
}
})
this.$wsApi.onClose((e) => {
console.log(e);
if (e.code == 1006) {
//
this.$message.error("连接已断开,请重新登录");
location.href = "/";
}, 1000)
} else if (cmd == 3) {
//
msgInfo.selfSend = msgInfo.sendId==this.$store.state.userStore.userInfo.id;
//
this.handlePrivateMessage(msgInfo);
} else if (cmd == 4) {
//
msgInfo.selfSend = msgInfo.sendId==this.$store.state.userStore.userInfo.id;
//
this.handleGroupMessage(msgInfo);
}
} else {
this.$wsApi.connect();
}
});
}).catch((e) => {
console.log("初始化失败",e);
})
this.$wsApi.onClose((e) => {
console.log(e);
if(e.code == 1006){
//
this.$message.error("连接已断开,请重新登录");
location.href = "/";
},
loadPrivateMessage(minId) {
this.$store.commit("loadingPrivateMsg",true)
this.$http({
url: "/message/private/loadMessage?minId=" + minId,
method: 'get'
}).then((msgInfos) => {
msgInfos.forEach((msgInfo) => {
msgInfo.selfSend = msgInfo.sendId == this.$store.state.userStore.userInfo.id;
let friendId = msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
let friend = this.$store.state.friendStore.friends.find((f) => f.id == friendId);
if(friend){
this.insertPrivateMessage(friend,msgInfo);
}
})
if (msgInfos.length == 100) {
//
this.loadPrivateMessage(msgInfos[99].id);
}else{
this.$wsApi.connect();
this.$store.commit("loadingPrivateMsg",false)
}
});
})
},
pullUnreadMessage() {
//
loadGroupMessage(minId) {
this.$store.commit("loadingGroupMsg",true)
this.$http({
url: "/message/private/pullUnreadMessage",
method: 'post'
});
//
this.$http({
url: "/message/group/pullUnreadMessage",
method: 'post'
});
url: "/message/group/loadMessage?minId=" + minId,
method: 'get'
}).then((msgInfos) => {
msgInfos.forEach((msgInfo) => {
msgInfo.selfSend = msgInfo.sendId == this.$store.state.userStore.userInfo.id;
let groupId = msgInfo.groupId;
let group = this.$store.state.groupStore.groups.find((g) => g.id == groupId);
if(group){
this.insertGroupMessage(group,msgInfo);
}
})
if (msgInfos.length == 100) {
//
this.loadGroupMessage(msgInfos[99].id);
}else{
this.$store.commit("loadingGroupMsg",false)
}
})
},
handlePrivateMessage(msg) {
//
let friendId = msg.selfSend?msg.recvId:msg.sendId;
let friend = this.$store.state.friendStore.friends.find((f) => f.id == friendId);
if (friend) {
this.insertPrivateMessage(friend, msg);
//
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
// id
let friendId = msg.selfSend ? msg.recvId : msg.sendId;
//
if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
if (msg.selfSend) {
//
let chatInfo = {
type: 'PRIVATE',
targetId: friendId
}
this.$store.commit("resetUnreadCount", chatInfo)
} else {
//
this.$store.commit("readedMessage", friendId)
}
return;
}
//
this.$http({
url: `/friend/find/${msg.sendId}`,
method: 'get'
}).then((friend) => {
this.loadFriendInfo(friendId).then((friend) => {
this.insertPrivateMessage(friend, msg);
this.$store.commit("addFriend", friend);
})
},
insertPrivateMessage(friend, msg) {
// webrtc
if (msg.type >= this.$enums.MESSAGE_TYPE.RTC_CALL &&
msg.type <= this.$enums.MESSAGE_TYPE.RTC_CANDIDATE) {
//
if (msg.type == this.$enums.MESSAGE_TYPE.RTC_CALL ||
msg.type == this.$enums.MESSAGE_TYPE.RTC_CANCEL) {
@ -178,23 +212,27 @@
//
this.$store.commit("insertMessage", msg);
//
!msg.selfSend && this.playAudioTip();
if(!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED){
this.playAudioTip();
}
},
handleGroupMessage(msg) {
//
let group = this.$store.state.groupStore.groups.find((g) => g.id == msg.groupId);
if (group) {
this.insertGroupMessage(group, msg);
//
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id;
let groupId = msg.groupId;
//
if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
//
let chatInfo = {
type: 'GROUP',
targetId: groupId
}
this.$store.commit("resetUnreadCount", chatInfo)
return;
}
//
this.$http({
url: `/group/find/${msg.groupId}`,
method: 'get'
}).then((group) => {
this.loadGroupInfo(groupId).then((group) => {
//
this.insertGroupMessage(group, msg);
this.$store.commit("addGroup", group);
})
},
insertGroupMessage(group, msg) {
@ -209,7 +247,9 @@
//
this.$store.commit("insertMessage", msg);
//
!msg.selfSend && this.playAudioTip();
if(!msg.selfSend && msg.status != this.$enums.MESSAGE_STATUS.READED){
this.playAudioTip();
}
},
handleExit() {
this.$wsApi.close();
@ -217,16 +257,52 @@
location.href = "/";
},
playAudioTip() {
let audio = new Audio();
let url = require(`@/assets/audio/tip.wav`);
audio.src = url;
audio.play();
if(new Date() - this.lastPlayAudioTime > 1000){
this.lastPlayAudioTime = new Date();
let audio = new Audio();
let url = require(`@/assets/audio/tip.wav`);
audio.src = url;
audio.play();
}
},
showSetting() {
this.showSettingDialog = true;
},
closeSetting() {
this.showSettingDialog = false;
},
loadFriendInfo(id) {
return new Promise((resolve, reject) => {
let friend = this.$store.state.friendStore.friends.find((f) => f.id == id);
if (friend) {
resolve(friend);
} else {
this.$http({
url: `/friend/find/${id}`,
method: 'get'
}).then((friend) => {
this.$store.commit("addFriend", friend);
resolve(friend)
})
}
});
},
loadGroupInfo(id) {
return new Promise((resolve, reject) => {
let group = this.$store.state.groupStore.groups.find((g) => g.id == id);
if (group) {
resolve(group);
} else {
this.$http({
url: `/group/find/${id}`,
method: 'get'
}).then((group) => {
resolve(group)
this.$store.commit("addGroup", group);
})
}
});
}
},
computed: {
@ -252,12 +328,7 @@
}
},
mounted() {
this.$http({
url: "/user/self",
methods: 'get'
}).then((userInfo) => {
this.init(userInfo);
})
this.init();
},
unmounted() {
this.$wsApi.close();
@ -310,7 +381,7 @@
}
}
.exit-box {
position: absolute;
@ -334,4 +405,4 @@
text-align: center;
}
</style>
</style>

170
im-ui/src/view/Login.vue

@ -1,27 +1,60 @@
<template>
<div class="login-view" >
<el-form :model="loginForm" status-icon :rules="rules" ref="loginForm" label-width="60px" class="web-ruleForm" @keyup.enter.native="submitForm('loginForm')">
<div class="login-brand">欢迎登陆</div>
<el-form-item label="终端" prop="userName" v-show="false">
<el-input type="terminal" v-model="loginForm.terminal" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="用户名" prop="userName">
<el-input type="userName" v-model="loginForm.userName" autocomplete="off"></el-input>
<div class="login-view">
<div class="login-intro">
<div>
<h3>盒子IM 2.0版本正式发布</h3>
<ul>
<li>发布uniapp移动版本支持移动端和web端同时在线多端消息同步</li>
<li>目前移动端仅兼容h5和微信小程序后续会继续兼容更多终端类型</li>
<li>页面风格升级表情包更新自动生成文字头像等</li>
</ul>
</div>
<div>
<h3>最近更新(2023-11-05)</h3>
<ul>
<li>聊天输入框支持粘贴截图</li>
<li>聊天消息支持显示已读未读状态</li>
<li>修改拉取离线消息机制:用户登录后,自动从服务器同步最近1个月的消息</li>
</ul>
</div>
<div>
<h3>项目依旧完全开源可内网部署如果项目对您有帮助,请帮忙点个star:</h3>
</div>
<div class="login-icons">
<a class="login-icon">
<img src="https://img.shields.io/badge/license-MIT-red" />
</a>
<a class="login-icon" href="https://gitee.com/bluexsx/box-im" target="_blank">
<img src="https://gitee.com/bluexsx/box-im/badge/star.svg" />
</a>
<a class="login-icon" href="https://gitee.com/bluexsx/box-im" target="_blank">
<img src="https://img.shields.io/github/stars/bluexsx/box-im.svg?style=flat&logo=GitHub" />
</a>
</div>
</div>
<el-form class="login-form" :model="loginForm" status-icon :rules="rules" ref="loginForm" label-width="60px"
@keyup.enter.native="submitForm('loginForm')">
<div class="login-brand">登陆盒子IM</div>
<el-form-item label="终端" prop="userName" v-show="false">
<el-input type="terminal" v-model="loginForm.terminal" autocomplete="off" ></el-input>
</el-form-item>
<el-form-item label="用户名" prop="userName">
<el-input type="userName" v-model="loginForm.userName" autocomplete="off" placeholder="用户名"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="loginForm.password" autocomplete="off" placeholder="密码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('loginForm')">登陆</el-button>
<el-button @click="resetForm('loginForm')">清空</el-button>
</el-form-item>
<div class="register">
<router-link to="/register">没有账号,前往注册</router-link>
</div>
</el-form>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="loginForm.password" autocomplete="off"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('loginForm')">登陆</el-button>
<el-button @click="resetForm('loginForm')">清空</el-button>
</el-form-item>
<div class="register">
<router-link to="/register">没有账号,前往注册</router-link>
</div>
</el-form>
</div>
</template>
@ -71,11 +104,11 @@
})
.then((data) => {
// cookie()
this.setCookie('username',this.loginForm.userName);
this.setCookie('password',this.loginForm.password);
this.setCookie('username', this.loginForm.userName);
this.setCookie('password', this.loginForm.password);
// token
sessionStorage.setItem("accessToken",data.accessToken);
sessionStorage.setItem("refreshToken",data.refreshToken);
sessionStorage.setItem("accessToken", data.accessToken);
sessionStorage.setItem("refreshToken", data.refreshToken);
this.$message.success("登陆成功");
this.$router.push("/home/chat");
})
@ -90,26 +123,27 @@
getCookie(name) {
let reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
let arr = document.cookie.match(reg)
if (arr){
return unescape(arr[2]);
if (arr) {
return unescape(arr[2]);
}
return '';
},
// cookie,vue便
setCookie (name, value, expiredays) {
var exdate = new Date();
exdate.setDate(exdate.getDate() + expiredays);
document.cookie = name + "=" + escape(value) + ((expiredays == null) ? "" : ";expires=" + exdate.toGMTString());
},
// cookie
delCookie (name) {
var exp = new Date();
exp.setTime(exp.getTime() - 1);
var cval = this.getCookie(name);
if (cval != null){
return '';
},
// cookie,vue便
setCookie(name, value, expiredays) {
var exdate = new Date();
exdate.setDate(exdate.getDate() + expiredays);
document.cookie = name + "=" + escape(value) + ((expiredays == null) ? "" : ";expires=" + exdate
.toGMTString());
},
// cookie
delCookie(name) {
var exp = new Date();
exp.setTime(exp.getTime() - 1);
var cval = this.getCookie(name);
if (cval != null) {
document.cookie = name + "=" + cval + ";expires=" + exp.toGMTString();
}
}
}
},
mounted() {
this.loginForm.userName = this.getCookie("username");
@ -124,21 +158,45 @@
position: relative;
display: flex;
justify-content: space-around;
align-items: center;
width: 100%;
height: 100%;
background: linear-gradient(#65807a, #182e3c);
background: rgb(232, 242, 255);
background-size: cover;
.web-ruleForm {
box-sizing: border-box;
padding: 10%;
.login-intro {
flex: 1;
padding: 40px;
max-width: 600px;
.login-title {
text-align: center;
font-weight: 600;
font-size: 30px;
}
.login-icons {
display: flex;
align-items: center;
.login-icon {
padding-left: 5px;
}
}
}
.login-form {
height: 340px;
padding: 20px;
margin-top: 150px ;
background: rgba(255,255,255,.75);
box-shadow: 0px 0px 1px #ccc;
border-radius: 5px;
width: 400px;
padding: 30px;
background: white;
opacity: 0.9;
box-shadow: 0px 0px 1px #ccc;
border-radius: 3%;
overflow: hidden;
border: 1px solid #ccc;
.login-brand {
line-height: 50px;
margin: 30px 0 40px 0;
@ -148,7 +206,7 @@
text-transform: uppercase;
text-align: center;
}
.register {
display: flex;
flex-direction: row-reverse;
@ -158,6 +216,4 @@
}
}
}
</style>
</style>

25
im-ui/src/view/Register.vue

@ -3,18 +3,18 @@
<div>
<el-form :model="registerForm" status-icon :rules="rules" ref="registerForm" label-width="80px" class="web-ruleForm">
<div class="register-brand">欢迎注册</div>
<div class="register-brand">欢迎成为盒子IM的用户</div>
<el-form-item label="用户名" prop="userName">
<el-input type="userName" v-model="registerForm.userName" autocomplete="off"></el-input>
<el-input type="userName" v-model="registerForm.userName" autocomplete="off" placeholder="用户名(登录使用)"></el-input>
</el-form-item>
<el-form-item label="昵称" prop="nickName">
<el-input type="nickName" v-model="registerForm.nickName" autocomplete="off"></el-input>
<el-input type="nickName" v-model="registerForm.nickName" autocomplete="off" placeholder="昵称"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="registerForm.password" autocomplete="off"></el-input>
<el-input type="password" v-model="registerForm.password" autocomplete="off" placeholder="密码"></el-input>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input type="password" v-model="registerForm.confirmPassword" autocomplete="off"></el-input>
<el-input type="password" v-model="registerForm.confirmPassword" autocomplete="off" placeholder="确认密码"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('registerForm')">注册</el-button>
@ -117,24 +117,21 @@
position: fixed;
display: flex;
justify-content: space-around;
align-items: center;
width: 100%;
height: 100%;
background: #466368;
background: linear-gradient(#65807a, #182e3c);
background-size: cover;
-webkit-user-select: none;
background-size: cover;
background: rgb(232, 242, 255);
.web-ruleForm {
width: 500px;
height: 430px;
height: 450px;
padding: 20px;
margin-top: 100px ;
background: rgba(255,255,255,.75);
background: white;
opacity: 0.9;
box-shadow: 0px 0px 1px #ccc;
border-radius: 3px;
overflow: hidden;
border-radius: 3%;
.register-brand {
line-height: 50px;

176
im-uniapp/App.vue

@ -18,6 +18,9 @@
this.initAudit();
// websocket
this.initWebSocket();
// 线
this.loadPrivateMessage(store.state.chatStore.privateMsgMaxId);
this.loadGroupMessage(store.state.chatStore.groupMsgMaxId);
}).catch((e) => {
console.log(e);
this.exit();
@ -28,10 +31,7 @@
let userId = store.state.userStore.userInfo.id;
wsApi.init(process.env.WS_URL, loginInfo.accessToken);
wsApi.connect();
wsApi.onOpen(()=>{
//
this.pullUnreadMessage();
})
wsApi.onOpen()
wsApi.onMessage((cmd, msgInfo) => {
if (cmd == 2) {
// 线
@ -41,58 +41,95 @@
})
this.exit();
} else if (cmd == 3) {
//
msgInfo.selfSend = userId == msgInfo.sendId;
//
//
this.handlePrivateMessage(msgInfo);
} else if (cmd == 4) {
//
msgInfo.selfSend = userId == msgInfo.sendId;
//
//
this.handleGroupMessage(msgInfo);
}
});
wsApi.onClose((res)=>{
wsApi.onClose((res) => {
// 10063000APP
if(res.code == 1006){
if (res.code == 1006) {
uni.showToast({
title: '连接已断开,请重新登录',
icon: 'none',
})
this.exit();
}else if(res.code != 3000){
} else if (res.code != 3000) {
//
wsApi.connect();
}
})
},
pullUnreadMessage() {
//
loadPrivateMessage(minId) {
store.commit("loadingPrivateMsg", true)
http({
url: "/message/private/pullUnreadMessage",
method: 'POST'
});
//
url: "/message/private/loadMessage?minId=" + minId,
method: 'get'
}).then((msgInfos) => {
msgInfos.forEach((msgInfo) => {
msgInfo.selfSend = msgInfo.sendId == store.state.userStore.userInfo.id;
let friendId = msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
let friend = store.state.friendStore.friends.find((f) => f.id == friendId);
if(friend){
this.insertPrivateMessage(friend,msgInfo);
}
})
if (msgInfos.length == 100) {
//
this.loadPrivateMessage(msgInfos[99].id);
} else {
store.commit("loadingPrivateMsg", false)
}
})
},
loadGroupMessage(minId) {
store.commit("loadingGroupMsg", true)
http({
url: "/message/group/pullUnreadMessage",
method: 'POST'
});
url: "/message/group/loadMessage?minId=" + minId,
method: 'get'
}).then((msgInfos) => {
msgInfos.forEach((msgInfo) => {
msgInfo.selfSend = msgInfo.sendId == store.state.userStore.userInfo.id;
let groupId = msgInfo.groupId;
let group = store.state.groupStore.groups.find((g) => g.id == groupId);
if(group){
this.insertGroupMessage(group,msgInfo);
}
})
if (msgInfos.length == 100) {
//
this.loadGroupMessage(msgInfos[99].id);
} else {
store.commit("loadingGroupMsg", false)
}
})
},
handlePrivateMessage(msg) {
//
msg.selfSend = msg.sendId == store.state.userStore.userInfo.id;
// id
let friendId = msg.selfSend ? msg.recvId : msg.sendId;
let friend = store.state.friendStore.friends.find((f) => f.id == friendId);
if (!friend) {
http({
url: `/friend/find/${msg.sendId}`,
method: 'GET'
}).then((friend) => {
this.insertPrivateMessage(friend, msg);
store.commit("addFriend", friend);
})
} else {
//
this.insertPrivateMessage(friend, msg);
//
if (msg.type == enums.MESSAGE_TYPE.READED) {
if (msg.selfSend) {
//
let chatInfo = {
type: 'PRIVATE',
targetId: friendId
}
store.commit("resetUnreadCount", chatInfo)
} else {
//
store.commit("readedMessage", friendId)
}
return;
}
this.loadFriendInfo(friendId).then((friend) => {
this.insertPrivateMessage(friend, msg);
})
},
insertPrivateMessage(friend, msg) {
@ -115,19 +152,23 @@
},
handleGroupMessage(msg) {
let group = store.state.groupStore.groups.find((g) => g.id == msg.groupId);
if (!group) {
http({
url: `/group/find/${msg.groupId}`,
method: 'get'
}).then((group) => {
this.insertGroupMessage(group, msg);
store.commit("addGroup", group);
})
} else {
//
this.insertGroupMessage(group, msg);
//
msg.selfSend = msg.sendId == store.state.userStore.userInfo.id;
let groupId = msg.groupId;
//
if (msg.type == enums.MESSAGE_TYPE.READED) {
//
let chatInfo = {
type: 'GROUP',
targetId: groupId
}
store.commit("resetUnreadCount", chatInfo)
return;
}
this.loadGroupInfo(groupId).then((group) => {
//
this.insertGroupMessage(group, msg);
})
},
insertGroupMessage(group, msg) {
@ -144,11 +185,43 @@
//
!msg.selfSend && this.playAudioTip();
},
loadFriendInfo(id) {
return new Promise((resolve, reject) => {
let friend = store.state.friendStore.friends.find((f) => f.id == id);
if (friend) {
resolve(friend);
} else {
http({
url: `/friend/find/${id}`,
method: 'get'
}).then((friend) => {
store.commit("addFriend", friend);
resolve(friend)
})
}
});
},
loadGroupInfo(id) {
return new Promise((resolve, reject) => {
let group = store.state.groupStore.groups.find((g) => g.id == id);
if (group) {
resolve(group);
} else {
http({
url: `/group/find/${id}`,
method: 'get'
}).then((group) => {
resolve(group)
store.commit("addGroup", group);
})
}
});
},
exit() {
console.log("exit");
wsApi.close();
uni.removeStorageSync("loginInfo");
uni.navigateTo({
uni.reLaunch({
url: "/pages/login/login"
})
store.dispatch("unload");
@ -161,18 +234,18 @@
},
initAudit() {
console.log("initAudit")
if(store.state.userStore.userInfo.type == 1){
if (store.state.userStore.userInfo.type == 1) {
//
uni.setTabBarItem({
index: 2,
text: "群聊"
})
}else{
})
} else {
//
uni.setTabBarItem({
index: 2,
text: "搜索"
})
})
}
}
},
@ -214,5 +287,4 @@
// #endif
background-color: #f8f8f8;
}
</style>

23
im-uniapp/common/emotion.js

@ -7,41 +7,26 @@ const emoTextList = ['憨笑', '媚眼', '开心', '坏笑', '可怜', '爱心',
];
let emoImageUrlList = [];
// 备注:经过测试,小程序的<rich-text>无法显示相对路径的图片,所以在这里对图片提前全部转成绝对路径
// 提前初始化图片的url
for (let i = 0; i < emoTextList.length; i++) {
let path = `/static/emoji2/${i}.gif`;
uni.getImageInfo({
src: path,
success(res) {
emoImageUrlList[i] = res.path
},
fail(res) {
emoImageUrlList = path;
}
});
}
let transform = (content) => {
return content.replace(/\#[\u4E00-\u9FA5]{1,3}\;/gi, textToImg);
}
// 将匹配结果替换表情图片
let textToImg = (emoText) => {
let word = emoText.replace(/\#|\;/gi, '');
let idx = emoTextList.indexOf(word);
if (idx == -1) {
return "";
}
let path = textToPath(emoText);
// #ifdef MP
// 微信小程序不能有前面的'/'
path = path.slice(1);
// #endif
let img = `<img src="${path}" style="with:35px;height:35px;
margin: 0 -2px;vertical-align:bottom;"/>`;
margin: 0 -2px;vertical-align:bottom;"/>`;
return img;
}

11
im-uniapp/common/enums.js

@ -6,6 +6,7 @@ const MESSAGE_TYPE = {
AUDIO:3,
VIDEO:4,
RECALL:10,
READED:11,
TIP_TIME:20,
RTC_CALL: 101,
RTC_ACCEPT: 102,
@ -27,8 +28,16 @@ const TERMINAL_TYPE = {
APP: 1
}
const MESSAGE_STATUS = {
UNSEND: 0,
SENDED: 1,
RECALL:2,
READED:3
}
export {
MESSAGE_TYPE,
USER_STATE,
TERMINAL_TYPE
TERMINAL_TYPE,
MESSAGE_STATUS
}

3
im-uniapp/common/request.js

@ -43,9 +43,6 @@ const request = (options) => {
requestList.forEach(cb => cb());
requestList = [];
isRefreshToken = false;
// 保存token
console.log(res.data.data.accessToken)
// 重新发送刚才的请求
return resolve(request(options))

57
im-uniapp/components/chat-message-item/chat-message-item.vue

@ -2,12 +2,12 @@
<view class="chat-msg-item">
<view class="chat-msg-tip" v-if="msgInfo.type==$enums.MESSAGE_TYPE.RECALL">{{msgInfo.content}}</view>
<view class="chat-msg-tip" v-if="msgInfo.type==$enums.MESSAGE_TYPE.TIP_TIME">
{{$date.toTimeText(msgInfo.sendTime)}}</view>
{{$date.toTimeText(msgInfo.sendTime)}}
</view>
<view class="chat-msg-normal" v-if="msgInfo.type>=0 && msgInfo.type<10"
:class="{'chat-msg-mine':msgInfo.selfSend}">
<head-image class="avatar" :id="msgInfo.sendId" :url="headImage"
:name="showName" :size="80"></head-image>
<head-image class="avatar" :id="msgInfo.sendId" :url="headImage" :name="showName" :size="80"></head-image>
<view class="chat-msg-content" @longpress="onShowMenu($event)">
<view v-if="msgInfo.groupId && !msgInfo.selfSend" class="chat-msg-top">
<text>{{showName}}</text>
@ -18,8 +18,8 @@
:nodes="$emo.transform(msgInfo.content)"></rich-text>
<view class="chat-msg-image" v-if="msgInfo.type==$enums.MESSAGE_TYPE.IMAGE">
<view class="img-load-box">
<image class="send-image" mode="heightFix" :src="JSON.parse(msgInfo.content).thumbUrl" lazy-load="true"
@click.stop="onShowFullImage()">
<image class="send-image" mode="heightFix" :src="JSON.parse(msgInfo.content).thumbUrl"
lazy-load="true" @click.stop="onShowFullImage()">
</image>
<loading v-if="loading"></loading>
</view>
@ -40,6 +40,11 @@
<text title="发送失败" v-if="loadFail" @click="onSendFail"
class="send-fail iconfont icon-warning-circle-fill"></text>
</view>
<text class="chat-readed" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status==$enums.MESSAGE_STATUS.READED">已读</text>
<text class="chat-unread" v-show="msgInfo.selfSend && !msgInfo.groupId
&& msgInfo.status!=$enums.MESSAGE_STATUS.READED">未读</text>
<!--
<view class="chat-msg-voice" v-if="msgInfo.type==$enums.MESSAGE_TYPE.AUDIO" @click="onPlayVoice()">
<audio controls :src="JSON.parse(msgInfo.content).url"></audio>
@ -137,7 +142,7 @@
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "loading";
},
loadFail() {
return this.msgInfo.loadStatus && (this.msgInfo.loadStatus === "fail");
return this.msgInfo.loadStatus && this.msgInfo.loadStatus === "fail";
},
data() {
return JSON.parse(this.msgInfo.content)
@ -219,12 +224,13 @@
.chat-msg-bottom {
display: inline-block;
padding-right: 80rpx ;
.chat-msg-text {
padding-right: 80rpx;
.chat-msg-text {
position: relative;
line-height: 60rpx;
margin-top: 10rpx;
padding: 10rpx;
padding: 10rpx 20rpx;
background-color: #ebebf5;
border-radius: 10rpx;
color: #333;
@ -234,6 +240,7 @@
word-break: break-all;
white-space: pre-line;
box-shadow: 2px 2px 2px #c0c0f0;
&:after {
content: "";
position: absolute;
@ -244,7 +251,7 @@
border-style: solid dashed dashed;
border-color: #ebebf5 transparent transparent;
overflow: hidden;
border-width: 20rpx;
border-width: 18rpx;
}
}
@ -295,13 +302,14 @@
background-color: #eeeeee;
padding: 10px 15px;
box-shadow: 2px 2px 2px #c0c0c0;
.chat-file-info {
flex: 1;
height: 100%;
text-align: left;
font-size: 14px;
width: 300rpx;
.chat-file-name {
font-size: 16px;
font-weight: 600;
@ -325,14 +333,17 @@
}
.chat-msg-voice {
font-size: 14px;
cursor: pointer;
audio {
height: 45px;
padding: 5px 0;
}
.chat-unread {
font-size: 10px;
color: #f23c0f;
font-weight: 600;
}
.chat-readed {
font-size: 10px;
color: #ccc;
font-weight: 600;
}
}
}
@ -350,15 +361,17 @@
.chat-msg-content {
text-align: right;
.chat-msg-bottom {
padding-left: 80rpx ;
padding-left: 80rpx;
padding-right: 0;
.chat-msg-text {
margin-left: 10px;
background-color: #587ff0;
color: #fff;
box-shadow: 1px 1px 1px #ccc;
&:after {
left: auto;
right: -10px;

91
im-uniapp/components/loading/loading.vue

@ -1,49 +1,64 @@
<template>
<view class="loading-box">
<view class="rotate iconfont icon-loading" ></view>
</view>
<view class="loading-box" :style="loadingStyle">
<view class="rotate iconfont icon-loading" :style="icontStyle"></view>
<slot></slot>
</view>
</template>
<script>
export default {
data() {
return {};
},
methods: {
},
computed: {
}
};
import {
computed
} from "vue"
export default {
data() {
return {}
},
props: {
size: {
type: Number,
default: 100
},
mask: {
type: Boolean,
default: true
}
},
computed: {
icontStyle() {
return `font-size:${this.size}rpx`;
},
loadingStyle() {
return this.mask ? "background: rgba(0, 0, 0, 0.3);" : "";
}
}
}
</script>
<style>
.loading-box {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.4);
position: absolute;
left: 0;
top: 0;
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
}
<style lang="scss" scoped>
.loading-box {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
}
.rotate {
animation: rotate 2s ease-in-out infinite;
}
.rotate {
animation: rotate 2s ease-in-out infinite;
font-size: 100rpx;
}
@keyframes rotate {
from {
transform: rotate(0deg)
}
@keyframes rotate {
from {
transform: rotate(0deg)
}
to {
transform: rotate(360deg)
}
}
to {
transform: rotate(360deg)
}
}
</style>

86
im-uniapp/pages/chat/chat-box.vue

@ -6,9 +6,10 @@
<uni-icons class="btn-side right" type="more-filled" size="30" @click="onShowMore()"></uni-icons>
</view>
<view class="chat-msg" @click="switchChatTabBox('none',true)">
<scroll-view class="scroll-box" scroll-y="true" :scroll-into-view="'chat-item-'+scrollMsgIdx">
<scroll-view class="scroll-box" scroll-y="true" @scrolltoupper="onScrollToTop"
:scroll-into-view="'chat-item-'+scrollMsgIdx">
<view v-for="(msgInfo,idx) in chat.messages" :key="idx">
<chat-message-item :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
<chat-message-item v-if="idx>=showMinIdx" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)"
@recall="onRecallMessage" @delete="onDeleteMessage" @download="onDownloadFile"
:id="'chat-item-'+idx" :msgInfo="msgInfo">
</chat-message-item>
@ -32,8 +33,8 @@
<view class="chat-tab-bar" v-show="chatTabBox!='none' ||showKeyBoard " :style="{height:`${keyboardHeight}px`}">
<view v-if="chatTabBox == 'tools'" class="chat-tools">
<view class="chat-tools-item">
<image-upload :maxCount="9" sourceType="album" :onBefore="onUploadImageBefore" :onSuccess="onUploadImageSuccess"
:onError="onUploadImageFail">
<image-upload :maxCount="9" sourceType="album" :onBefore="onUploadImageBefore"
:onSuccess="onUploadImageSuccess" :onError="onUploadImageFail">
<view class="tool-icon iconfont icon-picture"></view>
</image-upload>
<view class="tool-name">相册</view>
@ -66,8 +67,9 @@
<scroll-view v-if="chatTabBox==='emo'" class="chat-emotion" scroll-y="true">
<view class="emotion-item-list">
<image class="emotion-item" :title="emoText" :src="$emo.textToPath(emoText)" v-for="(emoText, i) in $emo.emoTextList"
:key="i" @click="selectEmoji(emoText)" mode="aspectFit" lazy-load="true"></image>
<image class="emotion-item" :title="emoText" :src="$emo.textToPath(emoText)"
v-for="(emoText, i) in $emo.emoTextList" :key="i" @click="selectEmoji(emoText)" mode="aspectFit"
lazy-load="true"></image>
</view>
</scroll-view>
<view v-if="showKeyBoard"></view>
@ -89,11 +91,12 @@
scrollMsgIdx: 0, //
chatTabBox: 'none',
showKeyBoard: false,
keyboardHeight: 322
keyboardHeight: 322,
showMinIdx: 0 // showMinIdx
}
},
methods: {
showTip(){
showTip() {
uni.showToast({
title: "加班开发中...",
icon: "none"
@ -139,6 +142,7 @@
msgInfo.sendTime = new Date().getTime();
msgInfo.sendId = this.$store.state.userStore.userInfo.id;
msgInfo.selfSend = true;
msgInfo.status = this.$enums.MESSAGE_STATUS.UNSEND;
this.$store.commit("insertMessage", msgInfo);
this.sendText = "";
}).finally(() => {
@ -171,6 +175,7 @@
return;
}
this.$nextTick(() => {
console.log("scrollToMsgIdx",this.scrollMsgIdx)
this.scrollMsgIdx = idx;
});
@ -184,7 +189,7 @@
selectEmoji(emoText) {
this.sendText += `#${emoText};`;
},
onNavBack(){
onNavBack() {
uni.switchTab({
url: "/pages/chat/chat"
})
@ -211,7 +216,8 @@
sendTime: new Date().getTime(),
selfSend: true,
type: this.$enums.MESSAGE_TYPE.IMAGE,
loadStatus: "loading"
loadStatus: "loading",
status: this.$enums.MESSAGE_STATUS.UNSEND
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
@ -254,7 +260,8 @@
sendTime: new Date().getTime(),
selfSend: true,
type: this.$enums.MESSAGE_TYPE.FILE,
loadStatus: "loading"
loadStatus: "loading",
status: this.$enums.MESSAGE_STATUS.UNSEND
}
// id
this.fillTargetId(msgInfo, this.chat.targetId);
@ -318,6 +325,7 @@
msgInfo = JSON.parse(JSON.stringify(msgInfo));
msgInfo.type = this.$enums.MESSAGE_TYPE.RECALL;
msgInfo.content = '你撤回了一条消息';
msgInfo.status = this.$enums.MESSAGE_STATUS.RECALL;
this.$store.commit("insertMessage", msgInfo);
})
}
@ -337,7 +345,7 @@
});
}
},
fail(e){
fail(e) {
console.log(e);
uni.showToast({
title: "文件下载失败",
@ -346,17 +354,40 @@
}
});
},
onShowMore(){
onScrollToTop() {
// #ifdef MP
//
this.scrollToMsgIdx(this.showMinIdx);
// #endif
// 10
this.showMinIdx = this.showMinIdx > 10 ? this.showMinIdx - 10 : 0;
},
onShowMore() {
if (this.chat.type == "GROUP") {
uni.navigateTo({
url: "/pages/group/group-info?id="+this.group.id
url: "/pages/group/group-info?id=" + this.group.id
})
}else{
} else {
uni.navigateTo({
url: "/pages/common/user-info?id="+this.friend.id
url: "/pages/common/user-info?id=" + this.friend.id
})
}
},
readedMessage() {
if (this.chat.type == "GROUP") {
var url = `/message/group/readed?groupId=${this.chat.targetId}`
} else {
url = `/message/private/readed?friendId=${this.chat.targetId}`
}
this.$http({
url: url,
method: 'PUT'
}).then(() => {
this.$store.commit("resetUnreadCount", this.chat)
this.scrollToBottom();
})
},
loadGroup(groupId) {
this.$http({
url: `/group/find/${groupId}`,
@ -416,6 +447,9 @@
return 0;
}
return this.chat.messages.length;
},
unreadCount() {
return this.chat.unreadCount;
}
},
watch: {
@ -424,15 +458,28 @@
if (newSize > oldSize) {
this.scrollToBottom();
}
},
unreadCount: {
handler(newCount, oldCount) {
if (newCount > 0) {
//
this.readedMessage()
}
}
}
},
onLoad(options) {
//
this.chat = this.$store.state.chatStore.chats[options.chatIdx];
// 30
let size = this.chat.messages.length;
this.showMinIdx = size > 30 ? size - 30 : 0;
//
this.$store.commit("activeChat", options.chatIdx);
//
this.scrollToBottom();
//
this.readedMessage()
//
if (this.chat.type == "GROUP") {
this.loadGroup(this.chat.targetId);
@ -471,16 +518,17 @@
line-height: 60rpx;
font-size: 28rpx;
cursor: pointer;
&.left {
left: 30rpx;
}
&.right {
right: 30rpx;
}
}
}

26
im-uniapp/pages/chat/chat.vue

@ -1,10 +1,16 @@
<template>
<view class="tab-page">
<view class="chat-tip" v-if="$store.state.chatStore.chats.length==0">
<view v-if="loading" class="chat-loading" >
<loading :size="50" :mask="false">
<view>消息接收中...</view>
</loading>
</view>
<view class="chat-tip" v-if="!loading && chatStore.chats.length==0">
温馨提示您现在还没有任何聊天消息快跟您的好友发起聊天吧~
</view>
<scroll-view class="scroll-bar" v-else scroll-with-animation="true" scroll-y="true">
<view v-for="(chat,index) in $store.state.chatStore.chats" :key="index">
<view v-for="(chat,index) in chatStore.chats" :key="index">
<chat-item :chat="chat" :index="index" @longpress.native="onShowMenu($event,index)"></chat-item>
</view>
</scroll-view>
@ -97,12 +103,18 @@
}
},
computed: {
chatStore() {
return this.$store.state.chatStore;
},
unreadCount() {
let count = 0;
this.$store.state.chatStore.chats.forEach(chat => {
this.chatStore.chats.forEach(chat => {
count += chat.unreadCount;
})
return count;
},
loading() {
return this.chatStore.loadingGroupMsg || this.chatStore.loadingPrivateMsg
}
},
watch: {
@ -126,4 +138,12 @@
color: darkblue;
font-size: 30rpx;
}
.chat-loading {
display: block;
height: 100rpx;
background: white;
position: relative;
color: blue;
}
</style>

9
im-uniapp/pages/login/login.vue

@ -20,8 +20,8 @@
return {
loginForm: {
terminal: 1, // APP
userName: 'blue',
password: '123456'
userName: '',
password: ''
},
rules: {
userName: {
@ -48,6 +48,8 @@
}).then(data => {
console.log("登录成功,自动跳转到聊天页面...")
uni.setStorageSync("loginInfo", data);
uni.setStorageSync("userName",this.loginForm.userName)
uni.setStorageSync("password",this.loginForm.password)
// App.vue
getApp().init()
//
@ -58,13 +60,14 @@
}
},
onLoad() {
this.loginForm.userName = uni.getStorageSync("userName");
this.loginForm.password = uni.getStorageSync("password");
let loginInfo = uni.getStorageSync("loginInfo");
if (loginInfo) {
//
uni.switchTab({
url: "/pages/chat/chat"
})
}
}
}

141
im-uniapp/store/chatStore.js

@ -1,23 +1,33 @@
import {MESSAGE_TYPE} from '@/common/enums.js';
import {
MESSAGE_TYPE,
MESSAGE_STATUS
} from '@/common/enums.js';
import userStore from './userStore';
export default {
state: {
chats: []
activeIndex: -1,
chats: [],
privateMsgMaxId: 0,
groupMsgMaxId: 0,
loadingPrivateMsg: false,
loadingGroupMsg: false,
},
mutations: {
initChats(state,chats){
initChats(state, chatsData) {
state.chats = chatsData.chats ||[];
state.privateMsgMaxId = chatsData.privateMsgMaxId||0;
state.groupMsgMaxId = chatsData.groupMsgMaxId||0;
// 防止图片一直处在加载中状态
chats.forEach((chat)=>{
chat.messages.forEach((msg)=>{
if(msg.loadStatus == "loading"){
state.chats.forEach((chat) => {
chat.messages.forEach((msg) => {
if (msg.loadStatus == "loading") {
msg.loadStatus = "fail"
}
})
})
state.chats = chats;
},
openChat(state, chatInfo) {
let chat = null;
@ -49,10 +59,33 @@ export default {
},
activeChat(state, idx) {
state.activeIndex = idx;
if(idx>=0){
if (idx >= 0) {
state.chats[idx].unreadCount = 0;
}
},
resetUnreadCount(state, chatInfo) {
for (let idx in state.chats) {
if (state.chats[idx].type == chatInfo.type &&
state.chats[idx].targetId == chatInfo.targetId) {
state.chats[idx].unreadCount = 0;
}
}
this.commit("saveToStorage");
},
readedMessage(state, friendId) {
for (let idx in state.chats) {
if (state.chats[idx].type == 'PRIVATE' &&
state.chats[idx].targetId == friendId) {
state.chats[idx].messages.forEach((m) => {
console.log("readedMessage")
if (m.selfSend && m.status != MESSAGE_STATUS.RECALL) {
m.status = MESSAGE_STATUS.READED
}
})
}
}
this.commit("saveToStorage");
},
removeChat(state, idx) {
state.chats.splice(idx, 1);
this.commit("saveToStorage");
@ -73,7 +106,7 @@ export default {
}
}
},
moveTop(state,idx){
moveTop(state, idx) {
let chat = state.chats[idx];
// 放置头部
state.chats.splice(idx, 1);
@ -94,41 +127,44 @@ export default {
}
}
// 插入新的数据
if(msgInfo.type == MESSAGE_TYPE.IMAGE){
chat.lastContent = "[图片]";
}else if(msgInfo.type == MESSAGE_TYPE.FILE){
if (msgInfo.type == MESSAGE_TYPE.IMAGE) {
chat.lastContent = "[图片]";
} else if (msgInfo.type == MESSAGE_TYPE.FILE) {
chat.lastContent = "[文件]";
}else if(msgInfo.type == MESSAGE_TYPE.AUDIO){
} else if (msgInfo.type == MESSAGE_TYPE.AUDIO) {
chat.lastContent = "[语音]";
}else{
chat.lastContent = msgInfo.content;
} else {
chat.lastContent = msgInfo.content;
}
chat.lastSendTime = msgInfo.sendTime;
// 如果不是当前会话,未读加1
if(chatIdx != state.activeIndex){
// 未读加1
if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) {
chat.unreadCount++;
}
// 自己回复了消息,说明消息已读
if(msgInfo.selfSend){
chat.unreadCount=0;
// 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
state.privateMsgMaxId = msgInfo.id;
}
if (msgInfo.id && type == "GROUP" && msgInfo.id > state.groupMsgMaxId) {
state.groupMsgMaxId = msgInfo.id;
}
// 如果是已存在消息,则覆盖旧的消息数据
for (let idx in chat.messages) {
if(msgInfo.id && chat.messages[idx].id == msgInfo.id){
if (msgInfo.id && chat.messages[idx].id == msgInfo.id) {
Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage");
return;
}
// 正在发送中的消息可能没有id,通过发送时间判断
if(msgInfo.selfSend && chat.messages[idx].selfSend
&& chat.messages[idx].sendTime == msgInfo.sendTime){
if (msgInfo.selfSend && chat.messages[idx].selfSend &&
chat.messages[idx].sendTime == msgInfo.sendTime) {
Object.assign(chat.messages[idx], msgInfo);
this.commit("saveToStorage");
return;
}
}
// 间隔大于10分钟插入时间显示
if(!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600*1000)){
if (!chat.lastTimeTip || (chat.lastTimeTip < msgInfo.sendTime - 600 * 1000)) {
chat.messages.push({
sendTime: msgInfo.sendTime,
type: MESSAGE_TYPE.TIP_TIME,
@ -138,9 +174,9 @@ export default {
// 新的消息
chat.messages.push(msgInfo);
this.commit("saveToStorage");
},
deleteMessage(state, msgInfo){
deleteMessage(state, msgInfo) {
// 获取对方id或群id
let type = msgInfo.groupId ? 'GROUP' : 'PRIVATE';
let targetId = msgInfo.groupId ? msgInfo.groupId : msgInfo.selfSend ? msgInfo.recvId : msgInfo.sendId;
@ -152,16 +188,16 @@ export default {
break;
}
}
for (let idx in chat.messages) {
// 已经发送成功的,根据id删除
if(chat.messages[idx].id && chat.messages[idx].id == msgInfo.id){
if (chat.messages[idx].id && chat.messages[idx].id == msgInfo.id) {
chat.messages.splice(idx, 1);
break;
}
// 正在发送中的消息可能没有id,根据发送时间删除
if(msgInfo.selfSend && chat.messages[idx].selfSend
&&chat.messages[idx].sendTime == msgInfo.sendTime){
if (msgInfo.selfSend && chat.messages[idx].selfSend &&
chat.messages[idx].sendTime == msgInfo.sendTime) {
chat.messages.splice(idx, 1);
break;
}
@ -171,7 +207,7 @@ export default {
updateChatFromFriend(state, friend) {
for (let i in state.chats) {
let chat = state.chats[i];
if (chat.type=='PRIVATE' && chat.targetId == friend.id) {
if (chat.type == 'PRIVATE' && chat.targetId == friend.id) {
chat.headImage = friend.headImageThumb;
chat.showName = friend.nickName;
break;
@ -182,7 +218,7 @@ export default {
updateChatFromGroup(state, group) {
for (let i in state.chats) {
let chat = state.chats[i];
if (chat.type=='GROUP' && chat.targetId == group.id) {
if (chat.type == 'GROUP' && chat.targetId == group.id) {
chat.headImage = group.headImageThumb;
chat.showName = group.remark;
break;
@ -190,35 +226,50 @@ export default {
}
this.commit("saveToStorage");
},
saveToStorage(state){
loadingPrivateMsg(state, loadding) {
state.loadingPrivateMsg = loadding;
},
loadingGroupMsg(state, loadding) {
state.loadingGroupMsg = loadding;
},
saveToStorage(state) {
let userId = userStore.state.userInfo.id;
let key = "chats-" + userId;
let chatsData = {
privateMsgMaxId: state.privateMsgMaxId,
groupMsgMaxId: state.groupMsgMaxId,
chats: state.chats
}
uni.setStorage({
key:"chats-"+userId,
data: state.chats
key: key,
data: chatsData
})
},
clear(state){
clear(state) {
state.chats = [];
state.activeIndex = -1;
state.privateMsgMaxId = 0;
state.groupMsgMaxId = 0;
state.loadingPrivateMsg = false;
state.loadingGroupMsg = false;
}
},
actions:{
},
actions: {
loadChat(context) {
return new Promise((resolve, reject) => {
let userId = userStore.state.userInfo.id;
uni.getStorage({
key:"chats-"+userId,
key: "chats-" + userId,
success(res) {
context.commit("initChats",res.data);
context.commit("initChats", res.data);
resolve()
},
fail(e) {
// 不存在聊天记录,清空聊天列表
context.commit("initChats",[]);
resolve()
}
});
})
}
}
}
}
Loading…
Cancel
Save