Browse Source

!149 H5和小程序每个会话只保留1000条消息,防止storage溢出

Merge pull request !149 from blue/v_3.0.0
master
blue 10 months ago
committed by Gitee
parent
commit
64d0a2e939
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 168
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  2. 42
      im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
  3. 2
      im-platform/src/main/java/com/bx/implatform/task/schedule/FileExpireTask.java
  4. 3
      im-uniapp/.env.js
  5. 79
      im-uniapp/store/chatStore.js
  6. 5
      im-web/package.json
  7. 4
      im-web/src/components/chat/ChatAtBox.vue
  8. 75
      im-web/src/components/chat/ChatBox.vue
  9. 2
      im-web/src/components/chat/ChatGroupReaded.vue
  10. 8
      im-web/src/components/chat/ChatGroupSide.vue
  11. 2
      im-web/src/components/chat/ChatHistory.vue
  12. 2
      im-web/src/components/chat/ChatMessageItem.vue
  13. 21
      im-web/src/components/common/FullImage.vue
  14. 3
      im-web/src/components/common/HeadImage.vue
  15. 40
      im-web/src/components/common/UserInfo.vue
  16. 8
      im-web/src/components/friend/AddFriend.vue
  17. 2
      im-web/src/components/group/AddGroupMember.vue
  18. 2
      im-web/src/components/rtc/RtcGroupJoin.vue
  19. 2
      im-web/src/components/rtc/RtcPrivateVideo.vue
  20. 7
      im-web/src/components/setting/Setting.vue
  21. 19
      im-web/src/main.js
  22. 305
      im-web/src/store/chatStore.js
  23. 27
      im-web/src/store/configStore.js
  24. 117
      im-web/src/store/friendStore.js
  25. 66
      im-web/src/store/groupStore.js
  26. 33
      im-web/src/store/index.js
  27. 39
      im-web/src/store/uiStore.js
  28. 50
      im-web/src/store/userStore.js
  29. 9
      im-web/src/view/Chat.vue
  30. 21
      im-web/src/view/Friend.vue
  31. 35
      im-web/src/view/Group.vue
  32. 116
      im-web/src/view/Home.vue

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

@ -14,6 +14,7 @@ import com.bx.imcommon.enums.IMTerminalType;
import com.bx.imcommon.model.IMGroupMessage; import com.bx.imcommon.model.IMGroupMessage;
import com.bx.imcommon.model.IMUserInfo; import com.bx.imcommon.model.IMUserInfo;
import com.bx.imcommon.util.CommaTextUtils; import com.bx.imcommon.util.CommaTextUtils;
import com.bx.imcommon.util.ThreadPoolExecutorFactory;
import com.bx.implatform.contant.Constant; import com.bx.implatform.contant.Constant;
import com.bx.implatform.contant.RedisKey; import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.dto.GroupMessageDTO; import com.bx.implatform.dto.GroupMessageDTO;
@ -40,19 +41,21 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.util.*; import java.util.*;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, GroupMessage> implements public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, GroupMessage>
GroupMessageService { implements GroupMessageService {
private final GroupService groupService; private final GroupService groupService;
private final GroupMemberService groupMemberService; private final GroupMemberService groupMemberService;
private final RedisTemplate<String, Object> redisTemplate; private final RedisTemplate<String, Object> redisTemplate;
private final IMClient imClient; private final IMClient imClient;
private final SensitiveFilterUtil sensitiveFilterUtil; private final SensitiveFilterUtil sensitiveFilterUtil;
private static final ScheduledThreadPoolExecutor EXECUTOR = ThreadPoolExecutorFactory.getThreadPoolExecutor();
@Override @Override
public GroupMessageVO sendMessage(GroupMessageDTO dto) { public GroupMessageVO sendMessage(GroupMessageDTO dto) {
@ -67,7 +70,8 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId()); List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId());
if (dto.getReceipt() && userIds.size() > Constant.MAX_LARGE_GROUP_MEMBER) { if (dto.getReceipt() && userIds.size() > Constant.MAX_LARGE_GROUP_MEMBER) {
// 大群的回执消息过于消耗资源,不允许发送 // 大群的回执消息过于消耗资源,不允许发送
throw new GlobalException(String.format("当前群聊大于%s人,不支持发送回执消息", Constant.MAX_LARGE_GROUP_MEMBER)); throw new GlobalException(
String.format("当前群聊大于%s人,不支持发送回执消息", Constant.MAX_LARGE_GROUP_MEMBER));
} }
// 不用发给自己 // 不用发给自己
userIds = userIds.stream().filter(id -> !session.getUserId().equals(id)).collect(Collectors.toList()); userIds = userIds.stream().filter(id -> !session.getUserId().equals(id)).collect(Collectors.toList());
@ -78,11 +82,10 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
msg.setSendNickName(member.getShowNickName()); msg.setSendNickName(member.getShowNickName());
msg.setAtUserIds(CommaTextUtils.asText(dto.getAtUserIds())); msg.setAtUserIds(CommaTextUtils.asText(dto.getAtUserIds()));
// 过滤内容中的敏感词 // 过滤内容中的敏感词
if(MessageType.TEXT.code().equals(dto.getType())){ if (MessageType.TEXT.code().equals(dto.getType())) {
msg.setContent(sensitiveFilterUtil.filter(dto.getContent())); msg.setContent(sensitiveFilterUtil.filter(dto.getContent()));
} }
this.save(msg); this.save(msg);
// 群发 // 群发
GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class); GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class);
msgInfo.setAtUserIds(dto.getAtUserIds()); msgInfo.setAtUserIds(dto.getAtUserIds());
@ -140,97 +143,101 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
return msgInfo; return msgInfo;
} }
@Override @Override
public void pullOfflineMessage(Long minId) { public void pullOfflineMessage(Long minId) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
if(!imClient.isOnline(session.getUserId())){ if (!imClient.isOnline(session.getUserId())) {
throw new GlobalException("网络连接失败,无法拉取离线消息"); throw new GlobalException("网络连接失败,无法拉取离线消息");
} }
// 查询用户加入的群组 // 查询用户加入的群组
List<GroupMember> members = groupMemberService.findByUserId(session.getUserId()); List<GroupMember> members = groupMemberService.findByUserId(session.getUserId());
Map<Long, GroupMember> groupMemberMap = CollStreamUtil.toIdentityMap(members, GroupMember::getGroupId); Map<Long, GroupMember> groupMemberMap = CollStreamUtil.toIdentityMap(members, GroupMember::getGroupId);
Set<Long> groupIds = groupMemberMap.keySet(); Set<Long> groupIds = groupMemberMap.keySet();
if(CollectionUtil.isEmpty(groupIds)){ if (CollectionUtil.isEmpty(groupIds)) {
// 关闭加载中标志 // 关闭加载中标志
this.sendLoadingMessage(false); this.sendLoadingMessage(false, session);
return; return;
} }
// 开启加载中标志
this.sendLoadingMessage(true); // 只能拉取最近3个月的,移动端只拉最近一个月
// 只能拉取最近3个月的,最多拉取3000条
int months = session.getTerminal().equals(IMTerminalType.APP.code()) ? 1 : 3; int months = session.getTerminal().equals(IMTerminalType.APP.code()) ? 1 : 3;
Date minDate = DateUtils.addMonths(new Date(), -months); Date minDate = DateUtils.addMonths(new Date(), -months);
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery(); LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.gt(GroupMessage::getId, minId) wrapper.gt(GroupMessage::getId, minId).gt(GroupMessage::getSendTime, minDate)
.gt(GroupMessage::getSendTime, minDate) .in(GroupMessage::getGroupId, groupIds).orderByAsc(GroupMessage::getId);
.in(GroupMessage::getGroupId, groupIds)
.orderByAsc(GroupMessage::getId);
List<GroupMessage> messages = this.list(wrapper); List<GroupMessage> messages = this.list(wrapper);
// 通过群聊对消息进行分组 // 通过群聊对消息进行分组
Map<Long, List<GroupMessage>> messageGroupMap = messages.stream().collect(Collectors.groupingBy(GroupMessage::getGroupId)); Map<Long, List<GroupMessage>> messageGroupMap =
messages.stream().collect(Collectors.groupingBy(GroupMessage::getGroupId));
// 退群前的消息 // 退群前的消息
List<GroupMember> quitMembers = groupMemberService.findQuitInMonth(session.getUserId()); List<GroupMember> quitMembers = groupMemberService.findQuitInMonth(session.getUserId());
for(GroupMember quitMember: quitMembers){ for (GroupMember quitMember : quitMembers) {
wrapper = Wrappers.lambdaQuery(); wrapper = Wrappers.lambdaQuery();
wrapper.gt(GroupMessage::getId, minId) wrapper.gt(GroupMessage::getId, minId).between(GroupMessage::getSendTime, minDate, quitMember.getQuitTime())
.between(GroupMessage::getSendTime, minDate,quitMember.getQuitTime())
.eq(GroupMessage::getGroupId, quitMember.getGroupId()) .eq(GroupMessage::getGroupId, quitMember.getGroupId())
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code()) .ne(GroupMessage::getStatus, MessageStatus.RECALL.code()).orderByAsc(GroupMessage::getId);
.orderByAsc(GroupMessage::getId);
List<GroupMessage> groupMessages = this.list(wrapper); List<GroupMessage> groupMessages = this.list(wrapper);
messageGroupMap.put(quitMember.getGroupId(),groupMessages); messageGroupMap.put(quitMember.getGroupId(), groupMessages);
groupMemberMap.put(quitMember.getGroupId(),quitMember); groupMemberMap.put(quitMember.getGroupId(), quitMember);
} }
// 推送消息 EXECUTOR.execute(() -> {
AtomicInteger sendCount = new AtomicInteger(); // 开启加载中标志
messageGroupMap.forEach((groupId, groupMessages) -> { this.sendLoadingMessage(true, session);
// 填充消息状态 // 推送消息
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); AtomicInteger sendCount = new AtomicInteger();
Object o = redisTemplate.opsForHash().get(key, session.getUserId().toString()); messageGroupMap.forEach((groupId, groupMessages) -> {
long readedMaxId = Objects.isNull(o) ? -1 : Long.parseLong(o.toString()); // 第一次拉取时,一个群最多推送1w条消息,防止前端接收能力溢出导致卡顿
Map<Object, Object> maxIdMap = null; List<GroupMessage> sendMessages = groupMessages;
for(GroupMessage m:groupMessages){ if (minId <= 0 && groupMessages.size() > 10000) {
// 排除加群之前的消息 sendMessages = groupMessages.subList(groupMessages.size() - 10000, groupMessages.size());
GroupMember member = groupMemberMap.get(m.getGroupId());
if(DateUtil.compare(member.getCreatedTime(), m.getSendTime()) > 0){
continue;
} }
// 排除不需要接收的消息 // 填充消息状态
List<String> recvIds = CommaTextUtils.asList(m.getRecvIds()); String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
if(!recvIds.isEmpty() && !recvIds.contains(session.getUserId().toString())){ Object o = redisTemplate.opsForHash().get(key, session.getUserId().toString());
continue; long readedMaxId = Objects.isNull(o) ? -1 : Long.parseLong(o.toString());
} Map<Object, Object> maxIdMap = null;
// 组装vo for (GroupMessage m : sendMessages) {
GroupMessageVO vo = BeanUtils.copyProperties(m, GroupMessageVO.class); // 排除加群之前的消息
// 被@用户列表 GroupMember member = groupMemberMap.get(m.getGroupId());
List<String> atIds = CommaTextUtils.asList(m.getAtUserIds()); if (DateUtil.compare(member.getCreatedTime(), m.getSendTime()) > 0) {
vo.setAtUserIds(atIds.stream().map(Long::parseLong).collect(Collectors.toList())); continue;
// 填充状态 }
vo.setStatus(readedMaxId >= m.getId() ? MessageStatus.READED.code() : MessageStatus.UNSEND.code()); // 排除不需要接收的消息
// 针对回执消息填充已读人数 List<String> recvIds = CommaTextUtils.asList(m.getRecvIds());
if(m.getReceipt()){ if (!recvIds.isEmpty() && !recvIds.contains(session.getUserId().toString())) {
if(Objects.isNull(maxIdMap)) { continue;
maxIdMap = redisTemplate.opsForHash().entries(key); }
// 组装vo
GroupMessageVO vo = BeanUtils.copyProperties(m, GroupMessageVO.class);
// 被@用户列表
List<String> atIds = CommaTextUtils.asList(m.getAtUserIds());
vo.setAtUserIds(atIds.stream().map(Long::parseLong).collect(Collectors.toList()));
// 填充状态
vo.setStatus(readedMaxId >= m.getId() ? MessageStatus.READED.code() : MessageStatus.UNSEND.code());
// 针对回执消息填充已读人数
if (m.getReceipt()) {
if (Objects.isNull(maxIdMap)) {
maxIdMap = redisTemplate.opsForHash().entries(key);
}
int count = getReadedUserIds(maxIdMap, m.getId(), m.getSendId()).size();
vo.setReadedCount(count);
} }
int count = getReadedUserIds(maxIdMap, m.getId(),m.getSendId()).size(); // 推送
vo.setReadedCount(count); IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>();
sendMessage.setSender(new IMUserInfo(m.getSendId(), IMTerminalType.WEB.code()));
sendMessage.setRecvIds(Arrays.asList(session.getUserId()));
sendMessage.setRecvTerminals(Arrays.asList(session.getTerminal()));
sendMessage.setSendResult(false);
sendMessage.setSendToSelf(false);
sendMessage.setData(vo);
imClient.sendGroupMessage(sendMessage);
sendCount.getAndIncrement();
} }
// 推送 });
IMGroupMessage<GroupMessageVO> sendMessage = new IMGroupMessage<>(); // 关闭加载中标志
sendMessage.setSender(new IMUserInfo(m.getSendId(), IMTerminalType.WEB.code())); this.sendLoadingMessage(false, session);
sendMessage.setRecvIds(Arrays.asList(session.getUserId())); log.info("拉取离线群聊消息,用户id:{},数量:{}", session.getUserId(), sendCount.get());
sendMessage.setRecvTerminals(Arrays.asList(session.getTerminal()));
sendMessage.setSendResult(false);
sendMessage.setSendToSelf(false);
sendMessage.setData(vo);
imClient.sendGroupMessage(sendMessage);
sendCount.getAndIncrement();
}
}); });
// 关闭加载中标志
this.sendLoadingMessage(false);
log.info("拉取离线群聊消息,用户id:{},数量:{}",session.getUserId(),sendCount.get());
} }
@Override @Override
@ -238,10 +245,8 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
// 取出最后的消息id // 取出最后的消息id
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery(); LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.eq(GroupMessage::getGroupId, groupId) wrapper.eq(GroupMessage::getGroupId, groupId).orderByDesc(GroupMessage::getId).last("limit 1")
.orderByDesc(GroupMessage::getId) .select(GroupMessage::getId);
.last("limit 1")
.select(GroupMessage::getId);
GroupMessage message = this.getOne(wrapper); GroupMessage message = this.getOne(wrapper);
if (Objects.isNull(message)) { if (Objects.isNull(message)) {
return; return;
@ -276,9 +281,10 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
List<Long> userIds = groupMemberService.findUserIdsByGroupId(groupId); List<Long> userIds = groupMemberService.findUserIdsByGroupId(groupId);
Map<Object, Object> maxIdMap = redisTemplate.opsForHash().entries(key); Map<Object, Object> maxIdMap = redisTemplate.opsForHash().entries(key);
for (GroupMessage receiptMessage : receiptMessages) { for (GroupMessage receiptMessage : receiptMessages) {
Integer readedCount = getReadedUserIds(maxIdMap, receiptMessage.getId(),receiptMessage.getSendId()).size(); Integer readedCount =
getReadedUserIds(maxIdMap, receiptMessage.getId(), receiptMessage.getSendId()).size();
// 如果所有人都已读,记录回执消息完成标记 // 如果所有人都已读,记录回执消息完成标记
if(readedCount >= userIds.size() - 1){ if (readedCount >= userIds.size() - 1) {
receiptMessage.setReceiptOk(true); receiptMessage.setReceiptOk(true);
this.updateById(receiptMessage); this.updateById(receiptMessage);
} }
@ -311,12 +317,12 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
if (Objects.isNull(member) || member.getQuit()) { if (Objects.isNull(member) || member.getQuit()) {
throw new GlobalException("您已不在群聊里面"); throw new GlobalException("您已不在群聊里面");
} }
// 已读位置key // 已读位置key
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId); String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
// 一次获取所有用户的已读位置 // 一次获取所有用户的已读位置
Map<Object, Object> maxIdMap = redisTemplate.opsForHash().entries(key); Map<Object, Object> maxIdMap = redisTemplate.opsForHash().entries(key);
// 返回已读用户的id集合 // 返回已读用户的id集合
return getReadedUserIds(maxIdMap, message.getId(),message.getSendId()); return getReadedUserIds(maxIdMap, message.getId(), message.getSendId());
} }
@Override @Override
@ -333,10 +339,11 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
// 查询聊天记录,只查询加入群聊时间之后的消息 // 查询聊天记录,只查询加入群聊时间之后的消息
QueryWrapper<GroupMessage> wrapper = new QueryWrapper<>(); 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); .ne(GroupMessage::getStatus, MessageStatus.RECALL.code()).orderByDesc(GroupMessage::getId)
.last("limit " + stIdx + "," + size);
List<GroupMessage> messages = this.list(wrapper); List<GroupMessage> messages = this.list(wrapper);
List<GroupMessageVO> messageInfos = List<GroupMessageVO> messageInfos =
messages.stream().map(m -> BeanUtils.copyProperties(m, GroupMessageVO.class)).collect(Collectors.toList()); messages.stream().map(m -> BeanUtils.copyProperties(m, GroupMessageVO.class)).collect(Collectors.toList());
log.info("拉取群聊记录,用户id:{},群聊id:{},数量:{}", userId, groupId, messageInfos.size()); log.info("拉取群聊记录,用户id:{},群聊id:{},数量:{}", userId, groupId, messageInfos.size());
return messageInfos; return messageInfos;
} }
@ -354,8 +361,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
return userIds; return userIds;
} }
private void sendLoadingMessage(Boolean isLoadding){ private void sendLoadingMessage(Boolean isLoadding, UserSession session) {
UserSession session = SessionContext.getSession();
GroupMessageVO msgInfo = new GroupMessageVO(); GroupMessageVO msgInfo = new GroupMessageVO();
msgInfo.setType(MessageType.LOADING.code()); msgInfo.setType(MessageType.LOADING.code());
msgInfo.setContent(isLoadding.toString()); msgInfo.setContent(isLoadding.toString());

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

@ -10,6 +10,7 @@ import com.bx.imcommon.contant.IMConstant;
import com.bx.imcommon.enums.IMTerminalType; import com.bx.imcommon.enums.IMTerminalType;
import com.bx.imcommon.model.IMPrivateMessage; import com.bx.imcommon.model.IMPrivateMessage;
import com.bx.imcommon.model.IMUserInfo; import com.bx.imcommon.model.IMUserInfo;
import com.bx.imcommon.util.ThreadPoolExecutorFactory;
import com.bx.implatform.dto.PrivateMessageDTO; import com.bx.implatform.dto.PrivateMessageDTO;
import com.bx.implatform.entity.PrivateMessage; import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.enums.MessageStatus; import com.bx.implatform.enums.MessageStatus;
@ -32,6 +33,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@ -43,6 +45,7 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
private final FriendService friendService; private final FriendService friendService;
private final IMClient imClient; private final IMClient imClient;
private final SensitiveFilterUtil sensitiveFilterUtil; private final SensitiveFilterUtil sensitiveFilterUtil;
private static final ScheduledThreadPoolExecutor EXECUTOR = ThreadPoolExecutorFactory.getThreadPoolExecutor();
@Override @Override
public PrivateMessageVO sendMessage(PrivateMessageDTO dto) { public PrivateMessageVO sendMessage(PrivateMessageDTO dto) {
@ -135,8 +138,6 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
@Override @Override
public void pullOfflineMessage(Long minId) { public void pullOfflineMessage(Long minId) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
// 开启加载中标志
this.sendLoadingMessage(true);
// 获取当前用户的消息 // 获取当前用户的消息
LambdaQueryWrapper<PrivateMessage> wrapper = Wrappers.lambdaQuery(); LambdaQueryWrapper<PrivateMessage> wrapper = Wrappers.lambdaQuery();
// 只能拉取最近3个月的消息,移动端只拉取一个月消息 // 只能拉取最近3个月的消息,移动端只拉取一个月消息
@ -148,21 +149,25 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
.eq(PrivateMessage::getRecvId, session.getUserId())); .eq(PrivateMessage::getRecvId, session.getUserId()));
wrapper.orderByAsc(PrivateMessage::getId); wrapper.orderByAsc(PrivateMessage::getId);
List<PrivateMessage> messages = this.list(wrapper); List<PrivateMessage> messages = this.list(wrapper);
// 推送消息 // 异步推送消息
for (PrivateMessage m : messages) { EXECUTOR.execute(() -> {
PrivateMessageVO vo = BeanUtils.copyProperties(m, PrivateMessageVO.class); // 开启加载中标志
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>(); this.sendLoadingMessage(true, session);
sendMessage.setSender(new IMUserInfo(m.getSendId(), IMTerminalType.WEB.code())); for (PrivateMessage m : messages) {
sendMessage.setRecvId(session.getUserId()); PrivateMessageVO vo = BeanUtils.copyProperties(m, PrivateMessageVO.class);
sendMessage.setRecvTerminals(List.of(session.getTerminal())); IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSendToSelf(false); sendMessage.setSender(new IMUserInfo(m.getSendId(), IMTerminalType.WEB.code()));
sendMessage.setData(vo); sendMessage.setRecvId(session.getUserId());
sendMessage.setSendResult(true); sendMessage.setRecvTerminals(List.of(session.getTerminal()));
imClient.sendPrivateMessage(sendMessage); sendMessage.setSendToSelf(false);
} sendMessage.setData(vo);
// 关闭加载中标志 sendMessage.setSendResult(true);
this.sendLoadingMessage(false); imClient.sendPrivateMessage(sendMessage);
log.info("拉取私聊消息,用户id:{},数量:{}", session.getUserId(), messages.size()); }
// 关闭加载中标志
this.sendLoadingMessage(false, session);
log.info("拉取私聊消息,用户id:{},数量:{}", session.getUserId(), messages.size());
});
} }
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@ -215,8 +220,7 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
return message.getId(); return message.getId();
} }
private void sendLoadingMessage(Boolean isLoadding) { private void sendLoadingMessage(Boolean isLoadding, UserSession session) {
UserSession session = SessionContext.getSession();
PrivateMessageVO msgInfo = new PrivateMessageVO(); PrivateMessageVO msgInfo = new PrivateMessageVO();
msgInfo.setType(MessageType.LOADING.code()); msgInfo.setType(MessageType.LOADING.code());
msgInfo.setContent(isLoadding.toString()); msgInfo.setContent(isLoadding.toString());

2
im-platform/src/main/java/com/bx/implatform/task/schedule/FileExpireTask.java

@ -34,7 +34,7 @@ public class FileExpireTask {
private final MinioProperties minioProps; private final MinioProperties minioProps;
@RedisLock(prefixKey = RedisKey.IM_LOCK_FILE_TASK) @RedisLock(prefixKey = RedisKey.IM_LOCK_FILE_TASK)
@Scheduled(cron = "0 * * * * ?") @Scheduled(cron = "0 0 3 * * ?")
public void run() { public void run() {
log.info("【定时任务】过期文件处理..."); log.info("【定时任务】过期文件处理...");
int batchSize = 100; int batchSize = 100;

3
im-uniapp/.env.js

@ -1,9 +1,6 @@
//设置环境(打包前修改此变量) //设置环境(打包前修改此变量)
const ENV = "DEV"; const ENV = "DEV";
const UNI_APP = {} const UNI_APP = {}
// 每个会话最大消息缓存数量,-1表示不限制
UNI_APP.MAX_MESSAGE_SIZE = 3000;
// 表情包路径 // 表情包路径
UNI_APP.EMO_URL = "/static/emoji/"; UNI_APP.EMO_URL = "/static/emoji/";
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN

79
im-uniapp/store/chatStore.js

@ -1,7 +1,6 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { MESSAGE_TYPE, MESSAGE_STATUS } from '@/common/enums.js'; import { MESSAGE_TYPE, MESSAGE_STATUS } from '@/common/enums.js';
import useUserStore from './userStore'; import useUserStore from './userStore';
import UNI_APP from '../.env';
let cacheChats = []; let cacheChats = [];
export default defineStore('chatStore', { export default defineStore('chatStore', {
@ -20,10 +19,6 @@ export default defineStore('chatStore', {
this.chats = []; this.chats = [];
for (let chat of chatsData.chats) { for (let chat of chatsData.chats) {
chat.stored = false; chat.stored = false;
// 清理多余的消息,避免消息过多导致卡顿
if (UNI_APP.MAX_MESSAGE_SIZE > 0 && chat.messages.length > UNI_APP.MAX_MESSAGE_SIZE) {
chat.messages = chat.messages.slice(0, UNI_APP.MAX_MESSAGE_SIZE);
}
// 暂存至缓冲区 // 暂存至缓冲区
cacheChats.push(JSON.parse(JSON.stringify(chat))); cacheChats.push(JSON.parse(JSON.stringify(chat)));
// 加载期间显示只前15个会话做做样子,一切都为了加快初始化时间 // 加载期间显示只前15个会话做做样子,一切都为了加快初始化时间
@ -64,6 +59,7 @@ export default defineStore('chatStore', {
lastContent: "", lastContent: "",
lastSendTime: new Date().getTime(), lastSendTime: new Date().getTime(),
unreadCount: 0, unreadCount: 0,
hotMinIdx: 0,
messages: [], messages: [],
atMe: false, atMe: false,
atAll: false, atAll: false,
@ -244,24 +240,28 @@ export default defineStore('chatStore', {
deleteMessage(msgInfo, chatInfo) { deleteMessage(msgInfo, chatInfo) {
// 获取对方id或群id // 获取对方id或群id
let chat = this.findChat(chatInfo); let chat = this.findChat(chatInfo);
let isColdMessage = false;
for (let idx in chat.messages) { for (let idx in chat.messages) {
// 已经发送成功的,根据id删除 // 已经发送成功的,根据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); chat.messages.splice(idx, 1);
isColdMessage = idx < chat.hotMinIdx;
break; break;
} }
// 正在发送中的消息可能没有id,只有临时id // 正在发送中的消息可能没有id,只有临时id
if (chat.messages[idx].tmpId && chat.messages[idx].tmpId == msgInfo.tmpId) { if (chat.messages[idx].tmpId && chat.messages[idx].tmpId == msgInfo.tmpId) {
chat.messages.splice(idx, 1); chat.messages.splice(idx, 1);
isColdMessage = idx < chat.hotMinIdx;
break; break;
} }
} }
chat.stored = false; chat.stored = false;
this.saveToStorage(); this.saveToStorage(isColdMessage);
}, },
recallMessage(msgInfo, chatInfo) { recallMessage(msgInfo, chatInfo) {
let chat = this.findChat(chatInfo); let chat = this.findChat(chatInfo);
if (!chat) return; if (!chat) return;
let isColdMessage = false;
// 要撤回的消息id // 要撤回的消息id
let id = msgInfo.content; let id = msgInfo.content;
let name = msgInfo.selfSend ? '你' : chat.type == 'PRIVATE' ? '对方' : msgInfo.sendNickName; let name = msgInfo.selfSend ? '你' : chat.type == 'PRIVATE' ? '对方' : msgInfo.sendNickName;
@ -279,6 +279,7 @@ export default defineStore('chatStore', {
if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) { if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) {
chat.unreadCount++; chat.unreadCount++;
} }
isColdMessage = idx < chat.hotMinIdx;
} }
// 被引用的消息也要撤回 // 被引用的消息也要撤回
if (m.quoteMessage && m.quoteMessage.id == msgInfo.id) { if (m.quoteMessage && m.quoteMessage.id == msgInfo.id) {
@ -288,7 +289,7 @@ export default defineStore('chatStore', {
} }
} }
chat.stored = false; chat.stored = false;
this.saveToStorage(); this.saveToStorage(isColdMessage);
}, },
updateChatFromFriend(friend) { updateChatFromFriend(friend) {
let chat = this.findChatByFriend(friend.id) let chat = this.findChatByFriend(friend.id)
@ -336,20 +337,31 @@ export default defineStore('chatStore', {
} }
}, },
refreshChats() { refreshChats() {
if (!cacheChats) { if (!cacheChats) return;
return;
}
// 排序 // 排序
cacheChats.sort((chat1, chat2) => { cacheChats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime);
return chat2.lastSendTime - chat1.lastSendTime; // #ifndef APP-PLUS
}); /**
* 由于h5和小程序的stroge只有5m,大约只能存储2w条消息
* 所以这里每个会话只保留1000条消息防止溢出
*/
cacheChats.forEach(chat =>{
if(chat.messages.length > 1000){
let idx = chat.messages.length - 1000;
chat.messages = chat.messages.slice(idx);
}
})
// #endif
// 记录热数据索引位置
cacheChats.forEach(chat => chat.hotMinIdx = chat.messages.length);
// 将消息一次性装载回来 // 将消息一次性装载回来
this.chats = cacheChats; this.chats = cacheChats;
// 清空缓存,不再使用 // 清空缓存,不再使用
cacheChats = null; cacheChats = null;
this.saveToStorage(); // 消息持久化
this.saveToStorage(true);
}, },
saveToStorage(state) { saveToStorage(withColdMessage) {
// 加载中不保存,防止卡顿 // 加载中不保存,防止卡顿
if (this.isLoading()) { if (this.isLoading()) {
return; return;
@ -362,12 +374,27 @@ export default defineStore('chatStore', {
this.chats.forEach((chat) => { this.chats.forEach((chat) => {
let chatKey = `${key}-${chat.type}-${chat.targetId}` let chatKey = `${key}-${chat.type}-${chat.targetId}`
if (!chat.stored) { if (!chat.stored) {
chat.stored = true;
if (chat.delete) { if (chat.delete) {
uni.removeStorageSync(chatKey); uni.removeStorageSync(chatKey);
} else { } else {
uni.setStorageSync(chatKey, chat); // 存储冷数据
if (withColdMessage) {
let coldChat = Object.assign({}, chat);
coldChat.messages = chat.messages.slice(0, chat.hotMinIdx);
uni.setStorageSync(chatKey, coldChat)
}
// 存储热消息
let hotKey = chatKey + '-hot';
if (chat.messages.length > chat.hotMinIdx) {
let hotChat = Object.assign({}, chat);
hotChat.messages = chat.messages.slice(chat.hotMinIdx)
uni.setStorageSync(hotKey, hotChat);
console.log("热数据:",hotChat.messages.length)
} else {
uni.removeStorageSync(hotKey);
}
} }
chat.stored = true;
} }
if (!chat.delete) { if (!chat.delete) {
chatKeys.push(chatKey); chatKeys.push(chatKey);
@ -391,20 +418,26 @@ export default defineStore('chatStore', {
this.loadingPrivateMsg = false; this.loadingPrivateMsg = false;
this.loadingGroupMsg = false; this.loadingGroupMsg = false;
}, },
loadChat(context) { loadChat() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let userStore = useUserStore(); let userStore = useUserStore();
let userId = userStore.userInfo.id; let userId = userStore.userInfo.id;
let chatsData = uni.getStorageSync("chats-app-" + userId) let chatsData = uni.getStorageSync("chats-app-" + userId)
if (chatsData) { if (chatsData) {
if (chatsData.chatKeys) { if (chatsData.chatKeys) {
let time = new Date().getTime();
chatsData.chats = []; chatsData.chats = [];
chatsData.chatKeys.forEach(key => { chatsData.chatKeys.forEach(key => {
let chat = uni.getStorageSync(key); let coldChat = uni.getStorageSync(key);
if (chat) { let hotChat = uni.getStorageSync(key + '-hot');
chatsData.chats.push(chat); if (!coldChat && hotChat) {
return;
}
// 冷热消息合并
let chat = Object.assign({}, coldChat, hotChat);
if (hotChat && coldChat) {
chat.messages = coldChat.messages.concat(hotChat.messages)
} }
chatsData.chats.push(chat);
}) })
} }
this.initChats(chatsData); this.initChats(chatsData);

5
im-web/package.json

@ -19,8 +19,7 @@
"vue": "2.7.16", "vue": "2.7.16",
"vue-axios": "3.5.2", "vue-axios": "3.5.2",
"vue-router": "3.6.5", "vue-router": "3.6.5",
"vuex": "3.6.2", "pinia": "^2.1.7"
"vuex-persist": "3.1.3"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "~4.5.12", "@vue/cli-plugin-babel": "~4.5.12",
@ -61,4 +60,4 @@
"last 2 versions", "last 2 versions",
"not dead" "not dead"
] ]
} }

4
im-web/src/components/chat/ChatAtBox.vue

@ -42,7 +42,7 @@ export default {
init() { init() {
this.$refs.scrollBox.wrap.scrollTop = 0; this.$refs.scrollBox.wrap.scrollTop = 0;
this.showMembers = []; this.showMembers = [];
let userId = this.$store.state.userStore.userInfo.id; let userId = this.userStore.userInfo.id;
let name = "全体成员"; let name = "全体成员";
if (this.ownerId == userId && name.startsWith(this.searchText)) { if (this.ownerId == userId && name.startsWith(this.searchText)) {
this.showMembers.push({ this.showMembers.push({
@ -108,7 +108,7 @@ export default {
}, },
computed: { computed: {
isOwner() { isOwner() {
return this.$store.state.userStore.userInfo.id == this.ownerId; return this.userStore.userInfo.id == this.ownerId;
} }
}, },
watch: { watch: {

75
im-web/src/components/chat/ChatBox.vue

@ -10,16 +10,13 @@
<el-container class="content-box"> <el-container class="content-box">
<el-main class="im-chat-main" id="chatScrollBox" @scroll="onScroll"> <el-main class="im-chat-main" id="chatScrollBox" @scroll="onScroll">
<div class="im-chat-box"> <div class="im-chat-box">
<ul> <div v-for="(msgInfo, idx) in showMessages" :key="showMinIdx + idx">
<li v-for="(msgInfo, idx) in chat.messages" :key="idx"> <chat-message-item @call="onCall(msgInfo.type)"
<chat-message-item v-if="idx >= showMinIdx" @call="onCall(msgInfo.type)" :mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)"
:mine="msgInfo.sendId == mine.id" :headImage="headImage(msgInfo)" :showName="showName(msgInfo)" :msgInfo="msgInfo" :groupMembers="groupMembers"
:showName="showName(msgInfo)" :msgInfo="msgInfo" @delete="deleteMessage" @recall="recallMessage">
:groupMembers="groupMembers" @delete="deleteMessage" </chat-message-item>
@recall="recallMessage"> </div>
</chat-message-item>
</li>
</ul>
</div> </div>
</el-main> </el-main>
<div v-if="!isInBottom" class="scroll-to-bottom" @click="scrollToBottom"> <div v-if="!isInBottom" class="scroll-to-bottom" @click="scrollToBottom">
@ -140,8 +137,8 @@ export default {
}, },
methods: { methods: {
moveChatToTop() { moveChatToTop() {
let chatIdx = this.$store.getters.findChatIdx(this.chat); let chatIdx = this.chatStore.findChatIdx(this.chat);
this.$store.commit("moveTop", chatIdx); this.chatStore.moveTop(chatIdx);
}, },
closeRefBox() { closeRefBox() {
this.$refs.emoBox.close(); this.$refs.emoBox.close();
@ -166,13 +163,13 @@ export default {
msgInfo.loadStatus = 'ok'; msgInfo.loadStatus = 'ok';
msgInfo.id = m.id; msgInfo.id = m.id;
this.isReceipt = false; this.isReceipt = false;
this.$store.commit("insertMessage", [msgInfo, file.chat]); this.chatStore.insertMessage(msgInfo, file.chat);
}) })
}, },
onImageFail(e, file) { onImageFail(e, file) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo)); let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.loadStatus = 'fail'; msgInfo.loadStatus = 'fail';
this.$store.commit("insertMessage", [msgInfo, file.chat]); this.chatStore.insertMessage(msgInfo, file.chat);
}, },
onImageBefore(file) { onImageBefore(file) {
// //
@ -201,7 +198,7 @@ export default {
// id // id
this.fillTargetId(msgInfo, this.chat.targetId); this.fillTargetId(msgInfo, this.chat.targetId);
// //
this.$store.commit("insertMessage", [msgInfo, this.chat]); this.chatStore.insertMessage(msgInfo, this.chat);
// //
this.moveChatToTop(); this.moveChatToTop();
// //
@ -224,13 +221,13 @@ export default {
msgInfo.id = m.id; msgInfo.id = m.id;
this.isReceipt = false; this.isReceipt = false;
this.refreshPlaceHolder(); this.refreshPlaceHolder();
this.$store.commit("insertMessage", [msgInfo, file.chat]); this.chatStore.insertMessage(msgInfo, file.chat);
}) })
}, },
onFileFail(e, file) { onFileFail(e, file) {
let msgInfo = JSON.parse(JSON.stringify(file.msgInfo)); let msgInfo = JSON.parse(JSON.stringify(file.msgInfo));
msgInfo.loadStatus = 'fail'; msgInfo.loadStatus = 'fail';
this.$store.commit("insertMessage", [msgInfo, file.chat]); this.chatStore.insertMessage(msgInfo, file.chat);
}, },
onFileBefore(file) { onFileBefore(file) {
// //
@ -259,7 +256,7 @@ export default {
// id // id
this.fillTargetId(msgInfo, this.chat.targetId); this.fillTargetId(msgInfo, this.chat.targetId);
// //
this.$store.commit("insertMessage", [msgInfo, this.chat]); this.chatStore.insertMessage(msgInfo, this.chat);
// //
this.moveChatToTop(); this.moveChatToTop();
// //
@ -330,7 +327,7 @@ export default {
} }
// //
let ids = [this.mine.id]; let ids = [this.mine.id];
let maxChannel = this.$store.state.configStore.webrtc.maxChannel; let maxChannel = this.configStore.webrtc.maxChannel;
this.$refs.rtcSel.open(maxChannel, ids, ids, []); this.$refs.rtcSel.open(maxChannel, ids, ids, []);
}, },
onInviteOk(members) { onInviteOk(members) {
@ -375,9 +372,9 @@ export default {
} }
// id // id
this.fillTargetId(msgInfo, this.chat.targetId); this.fillTargetId(msgInfo, this.chat.targetId);
this.sendMessageRequest(msgInfo).then((m) => { this.sendMessageRequest(msgInfo).then(m => {
m.selfSend = true; m.selfSend = true;
this.$store.commit("insertMessage", [m, this.chat]); this.chatStore.insertMessage(m, this.chat);
// //
this.moveChatToTop(); this.moveChatToTop();
// //
@ -462,7 +459,7 @@ export default {
this.lockMessage = true; this.lockMessage = true;
this.sendMessageRequest(msgInfo).then((m) => { this.sendMessageRequest(msgInfo).then((m) => {
m.selfSend = true; m.selfSend = true;
this.$store.commit("insertMessage", [m, this.chat]); this.chatStore.insertMessage(m, this.chat);
// //
this.moveChatToTop(); this.moveChatToTop();
}).finally(() => { }).finally(() => {
@ -487,7 +484,7 @@ export default {
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning' type: 'warning'
}).then(() => { }).then(() => {
this.$store.commit("deleteMessage", [msgInfo, this.chat]); this.chatStore.deleteMessage(msgInfo, this.chat);
}); });
}, },
recallMessage(msgInfo) { recallMessage(msgInfo) {
@ -503,7 +500,7 @@ export default {
}).then((m) => { }).then((m) => {
this.$message.success("消息已撤回"); this.$message.success("消息已撤回");
m.selfSend = true; m.selfSend = true;
this.$store.commit("recallMessage", [m, this.chat]); this.chatStore.recallMessage(m, this.chat);
}) })
}); });
}, },
@ -511,7 +508,7 @@ export default {
if (this.chat.unreadCount == 0) { if (this.chat.unreadCount == 0) {
return; return;
} }
this.$store.commit("resetUnreadCount", this.chat) this.chatStore.resetUnreadCount(this.chat)
if (this.chat.type == "GROUP") { if (this.chat.type == "GROUP") {
var url = `/message/group/readed?groupId=${this.chat.targetId}` var url = `/message/group/readed?groupId=${this.chat.targetId}`
} else { } else {
@ -520,14 +517,14 @@ export default {
this.$http({ this.$http({
url: url, url: url,
method: 'put' method: 'put'
}).then(() => {}) }).then(() => { })
}, },
loadReaded(fId) { loadReaded(fId) {
this.$http({ this.$http({
url: `/message/private/maxReadedId?friendId=${fId}`, url: `/message/private/maxReadedId?friendId=${fId}`,
method: 'get' method: 'get'
}).then((id) => { }).then((id) => {
this.$store.commit("readedMessage", { this.chatStore.readedMessage({
friendId: fId, friendId: fId,
maxId: id maxId: id
}); });
@ -539,8 +536,8 @@ export default {
method: 'get' method: 'get'
}).then((group) => { }).then((group) => {
this.group = group; this.group = group;
this.$store.commit("updateChatFromGroup", group); this.chatStore.updateChatFromGroup(group);
this.$store.commit("updateGroup", group); this.groupStore.updateGroup(group);
}); });
this.$http({ this.$http({
@ -557,10 +554,10 @@ export default {
friend.headImage = this.userInfo.headImageThumb; friend.headImage = this.userInfo.headImageThumb;
friend.nickName = this.userInfo.nickName; friend.nickName = this.userInfo.nickName;
friend.showNickName = friend.remarkNickName ? friend.remarkNickName : friend.nickName; friend.showNickName = friend.remarkNickName ? friend.remarkNickName : friend.nickName;
this.$store.commit("updateChatFromFriend", friend); this.chatStore.updateChatFromFriend(friend);
this.$store.commit("updateFriend", friend); this.friendStore.updateFriend(friend);
} else { } else {
this.$store.commit("updateChatFromUser", this.userInfo); this.chatStore.updateChatFromUser(this.userInfo);
} }
}, },
loadFriend(friendId) { loadFriend(friendId) {
@ -653,7 +650,7 @@ export default {
msgInfo.groupId = this.group.id; msgInfo.groupId = this.group.id;
msgInfo.content = "本群聊已被管理员封禁,原因:" + this.group.reason msgInfo.content = "本群聊已被管理员封禁,原因:" + this.group.reason
} }
this.$store.commit("insertMessage", [msgInfo, this.chat]); this.chatStore.insertMessage(msgInfo, this.chat);
}, },
generateId() { generateId() {
// id // id
@ -662,13 +659,13 @@ export default {
}, },
computed: { computed: {
mine() { mine() {
return this.$store.state.userStore.userInfo; return this.userStore.userInfo;
}, },
isFriend() { isFriend() {
return this.$store.getters.isFriend(this.userInfo.id); return this.friendStore.isFriend(this.userInfo.id);
}, },
friend() { friend() {
return this.$store.getters.findFriend(this.userInfo.id) return this.friendStore.findFriend(this.userInfo.id)
}, },
title() { title() {
let title = this.chat.showName; let title = this.chat.showName;
@ -684,6 +681,10 @@ export default {
unreadCount() { unreadCount() {
return this.chat.unreadCount; return this.chat.unreadCount;
}, },
showMessages() {
console.log("this.chat.messages.slice(this.showMinIdx):",this.chat.messages.slice(this.showMinIdx))
return this.chat.messages.slice(this.showMinIdx)
},
messageSize() { messageSize() {
if (!this.chat || !this.chat.messages) { if (!this.chat || !this.chat.messages) {
return 0; return 0;
@ -708,7 +709,7 @@ export default {
chat: { chat: {
handler(newChat, oldChat) { handler(newChat, oldChat) {
if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type || if (newChat.targetId > 0 && (!oldChat || newChat.type != oldChat.type ||
newChat.targetId != oldChat.targetId)) { newChat.targetId != oldChat.targetId)) {
this.userInfo = {} this.userInfo = {}
this.group = {}; this.group = {};
this.groupMembers = []; this.groupMembers = [];

2
im-web/src/components/chat/ChatGroupReaded.vue

@ -115,7 +115,7 @@ export default {
type: 'GROUP', type: 'GROUP',
targetId: this.msgInfo.groupId targetId: this.msgInfo.groupId
} }
this.$store.commit("updateMessage", [msgInfo, chatInfo]) this.chatStore.updateMessage(msgInfo, chatInfo)
}) })
} }
} }

8
im-web/src/components/chat/ChatGroupSide.vue

@ -124,7 +124,7 @@ export default {
data: vo data: vo
}).then((group) => { }).then((group) => {
this.editing = !this.editing this.editing = !this.editing
this.$store.commit("updateGroup", group); this.groupStore.updateGroup(group);
this.$emit('reload'); this.$emit('reload');
this.$message.success("修改成功"); this.$message.success("修改成功");
}) })
@ -139,8 +139,8 @@ export default {
url: `/group/quit/${this.group.id}`, url: `/group/quit/${this.group.id}`,
method: 'delete' method: 'delete'
}).then(() => { }).then(() => {
this.$store.commit("removeGroup", this.group.id); this.groupStore.removeGroup(this.group.id);
this.$store.commit("removeGroupChat", this.group.id); this.chatStore.removeGroupChat(this.group.id);
}); });
}) })
}, },
@ -156,7 +156,7 @@ export default {
}, },
computed: { computed: {
mine() { mine() {
return this.$store.state.userStore.userInfo; return this.userStore.userInfo;
}, },
ownerName() { ownerName() {
let member = this.groupMembers.find((m) => m.userId == this.group.ownerId); let member = this.groupMembers.find((m) => m.userId == this.group.ownerId);

2
im-web/src/components/chat/ChatHistory.vue

@ -126,7 +126,7 @@ export default {
}, },
computed: { computed: {
mine() { mine() {
return this.$store.state.userStore.userInfo; return this.userStore.userInfo;
}, },
histroyAction() { histroyAction() {
return `/message/${this.chat.type.toLowerCase()}/history`; return `/message/${this.chat.type.toLowerCase()}/history`;

2
im-web/src/components/chat/ChatMessageItem.vue

@ -126,7 +126,7 @@ export default {
showFullImageBox() { showFullImageBox() {
let imageUrl = JSON.parse(this.msgInfo.content).originUrl; let imageUrl = JSON.parse(this.msgInfo.content).originUrl;
if (imageUrl) { if (imageUrl) {
this.$store.commit('showFullImageBox', imageUrl); this.$eventBus.$emit("openFullImage", imageUrl);
} }
}, },
onPlayVoice() { onPlayVoice() {

21
im-web/src/components/common/FullImage.vue

@ -1,10 +1,10 @@
<template> <template>
<div class="full-image" v-show="visible" :before-close="onClose" :modal="true"> <div class="full-image" v-if="show" :before-close="close" :modal="true">
<div class="mask"></div> <div class="mask"></div>
<div class="image-box"> <div class="image-box">
<img :src="url" /> <img :src="url" />
</div> </div>
<div class="close" @click="onClose"><i class="el-icon-close"></i></div> <div class="close" @click="close"><i class="el-icon-close"></i></div>
</div> </div>
</template> </template>
@ -13,20 +13,17 @@ export default {
name: "fullImage", name: "fullImage",
data() { data() {
return { return {
fit: 'contain' show: false,
url: ''
} }
}, },
methods: { methods: {
onClose() { open(url) {
this.$emit("close"); this.show = true;
} this.url = url;
},
props: {
visible: {
type: Boolean
}, },
url: { close() {
type: String this.show = false;
} }
} }
} }

3
im-web/src/components/common/HeadImage.vue

@ -63,8 +63,7 @@ export default {
x: e.x + 30, x: e.x + 30,
y: e.y y: e.y
} }
this.$store.commit("setUserInfoBoxPos", pos); this.$eventBus.$emit("openUserInfo", user, pos);
this.$store.commit("showUserInfoBox", user);
}) })
} }
} }

40
im-web/src/components/common/UserInfo.vue

@ -1,5 +1,5 @@
<template> <template>
<div class="user-info" :style="{ left: pos.x + 'px', top: pos.y + 'px' }" @click.stop> <div v-if="show" class="user-info" :style="{ left: pos.x + 'px', top: pos.y + 'px' }" @click.stop>
<div class="user-info-box"> <div class="user-info-box">
<div class="avatar"> <div class="avatar">
<head-image :name="user.nickName" :url="user.headImageThumb" :size="60" :online="user.online" <head-image :name="user.nickName" :url="user.headImageThumb" :size="60" :online="user.online"
@ -39,18 +39,26 @@ export default {
}, },
data() { data() {
return { return {
show: false,
} user: {},
}, pos: {
props: { x: 0,
user: { y: 0
type: Object }
},
pos: {
type: Object
} }
}, },
methods: { methods: {
open(user, pos) {
this.show = true;
this.user = user;
let w = document.documentElement.clientWidth;
let h = document.documentElement.clientHeight;
this.pos.x = Math.min(pos.x, w - 350);
this.pos.y = Math.min(pos.y, h - 200);
},
close() {
this.show = false;
},
onSendMessage() { onSendMessage() {
let user = this.user; let user = this.user;
let chat = { let chat = {
@ -59,12 +67,12 @@ export default {
showName: user.nickName, showName: user.nickName,
headImage: user.headImage, headImage: user.headImage,
}; };
this.$store.commit("openChat", chat); this.chatStore.openChat(chat);
this.$store.commit("activeChat", 0); this.chatStore.setActiveChat(0);
if (this.$route.path != "/home/chat") { if (this.$route.path != "/home/chat") {
this.$router.push("/home/chat"); this.$router.push("/home/chat");
} }
this.$emit("close"); this.show = false;
}, },
onAddFriend() { onAddFriend() {
this.$http({ this.$http({
@ -82,18 +90,18 @@ export default {
online: this.user.online, online: this.user.online,
deleted: false deleted: false
} }
this.$store.commit("addFriend", friend); this.friendStore.addFriend(friend);
}) })
}, },
showFullImage() { showFullImage() {
if (this.user.headImage) { if (this.user.headImage) {
this.$store.commit("showFullImageBox", this.user.headImage); this.$eventBus.$emit("openFullImage", this.user.headImage);
} }
} }
}, },
computed: { computed: {
isFriend() { isFriend() {
return this.$store.getters.isFriend(this.user.id); return this.friendStore.isFriend(this.user.id);
} }
} }
} }

8
im-web/src/components/friend/AddFriend.vue

@ -6,7 +6,7 @@
<i class="el-icon-search el-input__icon" slot="suffix" @click="onSearch()"> </i> <i class="el-icon-search el-input__icon" slot="suffix" @click="onSearch()"> </i>
</el-input> </el-input>
<el-scrollbar style="height:400px"> <el-scrollbar style="height:400px">
<div v-for="(user) in users" :key="user.id" v-show="user.id != $store.state.userStore.userInfo.id"> <div v-for="(user) in users" :key="user.id" v-show="user.id != userStore.userInfo.id">
<div class="item"> <div class="item">
<div class="avatar"> <div class="avatar">
<head-image :name="user.nickName" :url="user.headImage" :online="user.online"></head-image> <head-image :name="user.nickName" :url="user.headImage" :online="user.online"></head-image>
@ -15,7 +15,7 @@
<div class="nick-name"> <div class="nick-name">
<div>{{ user.nickName }}</div> <div>{{ user.nickName }}</div>
<div :class="user.online ? 'online-status online' : 'online-status'">{{ <div :class="user.online ? 'online-status online' : 'online-status'">{{
user.online ? "[在线]" :"[离线]"}}</div> user.online ? "[在线]" : "[离线]" }}</div>
</div> </div>
<div class="user-name"> <div class="user-name">
<div>用户名:{{ user.userName }}</div> <div>用户名:{{ user.userName }}</div>
@ -83,11 +83,11 @@ export default {
online: user.online, online: user.online,
deleted: false deleted: false
} }
this.$store.commit("addFriend", friend); this.friendStore.addFriend(friend);
}) })
}, },
isFriend(userId) { isFriend(userId) {
return this.$store.getters.isFriend(userId); return this.friendStore.isFriend(userId);
} }
} }
} }

2
im-web/src/components/group/AddGroupMember.vue

@ -55,7 +55,7 @@ export default {
open() { open() {
this.show = true; this.show = true;
this.friends = []; this.friends = [];
this.$store.state.friendStore.friends.forEach((f) => { this.friendStore.friends.forEach((f) => {
if (f.deleted) { if (f.deleted) {
return; return;
} }

2
im-web/src/components/rtc/RtcGroupJoin.vue

@ -52,7 +52,7 @@ export default {
onOk() { onOk() {
this.isShow = false; this.isShow = false;
let userInfos = this.rtcInfo.userInfos; let userInfos = this.rtcInfo.userInfos;
let mine = this.$store.state.userStore.userInfo; let mine = this.userStore.userInfo;
if (!userInfos.find((user) => user.id == mine.id)) { if (!userInfos.find((user) => user.id == mine.id)) {
// //
userInfos.push({ userInfos.push({

2
im-web/src/components/rtc/RtcPrivateVideo.vue

@ -383,7 +383,7 @@ export default {
return strTime; return strTime;
}, },
configuration() { configuration() {
const iceServers = this.$store.state.configStore.webrtc.iceServers; const iceServers = this.configStore.webrtc.iceServers;
return { return {
iceServers: iceServers iceServers: iceServers
} }

7
im-web/src/components/setting/Setting.vue

@ -56,7 +56,6 @@ export default {
} }
}, },
methods: { methods: {
onClose() { onClose() {
this.$emit("close"); this.$emit("close");
}, },
@ -70,7 +69,7 @@ export default {
method: "put", method: "put",
data: this.userInfo data: this.userInfo
}).then(() => { }).then(() => {
this.$store.commit("setUserInfo", this.userInfo); this.userStore.setUserInfo(this.userInfo);
this.$emit("close"); this.$emit("close");
this.$message.success("修改成功"); this.$message.success("修改成功");
}) })
@ -92,9 +91,9 @@ export default {
} }
}, },
watch: { watch: {
visible: function(newData, oldData) { visible: function () {
// //
let mine = this.$store.state.userStore.userInfo; let mine = this.userStore.userInfo;
this.userInfo = JSON.parse(JSON.stringify(mine)); this.userInfo = JSON.parse(JSON.stringify(mine));
} }
} }

19
im-web/src/main.js

@ -4,18 +4,25 @@ import router from './router'
import ElementUI from 'element-ui'; import ElementUI from 'element-ui';
import './assets/style/im.scss'; import './assets/style/im.scss';
import './assets/iconfont/iconfont.css'; import './assets/iconfont/iconfont.css';
import { createPinia, PiniaVuePlugin } from 'pinia'
import httpRequest from './api/httpRequest'; import httpRequest from './api/httpRequest';
import * as socketApi from './api/wssocket'; import * as socketApi from './api/wssocket';
import * as messageType from './api/messageType'; import * as messageType from './api/messageType';
import emotion from './api/emotion.js'; import emotion from './api/emotion.js';
import url from './api/url.js'; import url from './api/url.js';
import element from './api/element.js'; import element from './api/element.js';
import store from './store';
import * as enums from './api/enums.js'; import * as enums from './api/enums.js';
import * as date from './api/date.js'; import * as date from './api/date.js';
import './utils/directive/dialogDrag'; import './utils/directive/dialogDrag';
import useChatStore from './store/chatStore.js'
import useFriendStore from './store/friendStore.js'
import useGroupStore from './store/groupStore.js'
import useUserStore from './store/userStore.js'
import useConfigStore from './store/configStore.js'
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
Vue.use(ElementUI); Vue.use(ElementUI);
// 挂载全局 // 挂载全局
Vue.prototype.$wsApi = socketApi; Vue.prototype.$wsApi = socketApi;
@ -33,7 +40,13 @@ new Vue({
el: '#app', el: '#app',
// 配置路由 // 配置路由
router, router,
store, pinia,
render: h => h(App) render: h => h(App)
}) })
// 挂载全局的pinia
Vue.prototype.chatStore = useChatStore();
Vue.prototype.friendStore = useFriendStore();
Vue.prototype.groupStore = useGroupStore();
Vue.prototype.userStore = useUserStore();
Vue.prototype.configStore = useConfigStore();

305
im-web/src/store/chatStore.js

@ -1,26 +1,42 @@
import { defineStore } from 'pinia';
import { MESSAGE_TYPE, MESSAGE_STATUS } from "../api/enums.js" import { MESSAGE_TYPE, MESSAGE_STATUS } from "../api/enums.js"
import userStore from './userStore'; import useUserStore from './userStore.js';
import localForage from 'localforage'; import localForage from 'localforage';
/* 线cacheChats, /**
待所有离线消息拉取完成后再统一放至vuex中进行渲染*/ * 优化1(冷热消息分区):
* 热消息登录后的消息
* 冷消息: 登录前的消息
* 每个会话的冷热消息分别用一个key进行存储当有新的消息时只更新热消息key冷消息key保持不变
* 由于热消息数量不会很大所以localForage.setItem耗时很低可以防止消息过多时出现卡顿的情况
*
* 优化2(延迟渲染):
* 拉取消息时如果直接用state.chats接收页面就开始渲染一边渲染页面一边大量接消息会导致很严重的卡顿
* 为了加速拉取离线消息效率拉取时消息暂时存储到cacheChats,等待所有离线消息拉取完成后再统一放至state中进行渲染
*
* 优化3(pinia代替vuex)
* 实测pinia的远超vuex,且语法更简洁清晰
*
* */
let cacheChats = []; let cacheChats = [];
export default { export default defineStore('chatStore', {
state: { state: () => {
activeChat: null, return {
privateMsgMaxId: 0, activeChat: null,
groupMsgMaxId: 0, privateMsgMaxId: 0,
loadingPrivateMsg: false, groupMsgMaxId: 0,
loadingGroupMsg: false, loadingPrivateMsg: false,
chats: [] loadingGroupMsg: false,
chats: []
}
}, },
actions: {
mutations: { initChats(chatsData) {
initChats(state, chatsData) { this.chats = [];
state.chats = []; this.privateMsgMaxId = chatsData.privateMsgMaxId || 0;
state.privateMsgMaxId = chatsData.privateMsgMaxId || 0; this.groupMsgMaxId = chatsData.groupMsgMaxId || 0;
state.groupMsgMaxId = chatsData.groupMsgMaxId || 0;
cacheChats = chatsData.chats || []; cacheChats = chatsData.chats || [];
// 防止图片一直处在加载中状态 // 防止图片一直处在加载中状态
cacheChats.forEach((chat) => { cacheChats.forEach((chat) => {
@ -31,15 +47,15 @@ export default {
}) })
}) })
}, },
openChat(state, chatInfo) { openChat(chatInfo) {
let chats = this.getters.findChats() let chats = this.findChats()
let chat = null; let chat = null;
for (let idx in chats) { for (let idx in chats) {
if (chats[idx].type == chatInfo.type && if (chats[idx].type == chatInfo.type &&
chats[idx].targetId === chatInfo.targetId) { chats[idx].targetId === chatInfo.targetId) {
chat = chats[idx]; chat = chats[idx];
// 放置头部 // 放置头部
this.commit("moveTop", idx) this.moveTop(idx)
break; break;
} }
} }
@ -53,6 +69,7 @@ export default {
lastContent: "", lastContent: "",
lastSendTime: new Date().getTime(), lastSendTime: new Date().getTime(),
unreadCount: 0, unreadCount: 0,
hotMinIdx: 0,
messages: [], messages: [],
atMe: false, atMe: false,
atAll: false, atAll: false,
@ -62,12 +79,12 @@ export default {
chats.unshift(chat); chats.unshift(chat);
} }
}, },
activeChat(state, idx) { setActiveChat(idx) {
let chats = this.getters.findChats(); let chats = this.findChats();
state.activeChat = chats[idx]; this.activeChat = chats[idx];
}, },
resetUnreadCount(state, chatInfo) { resetUnreadCount(chatInfo) {
let chats = this.getters.findChats(); let chats = this.findChats();
for (let idx in chats) { for (let idx in chats) {
if (chats[idx].type == chatInfo.type && if (chats[idx].type == chatInfo.type &&
chats[idx].targetId == chatInfo.targetId) { chats[idx].targetId == chatInfo.targetId) {
@ -75,13 +92,13 @@ export default {
chats[idx].atMe = false; chats[idx].atMe = false;
chats[idx].atAll = false; chats[idx].atAll = false;
chats[idx].stored = false; chats[idx].stored = false;
this.commit("saveToStorage"); this.saveToStorage();
break; break;
} }
} }
}, },
readedMessage(state, pos) { readedMessage(pos) {
let chat = this.getters.findChatByFriend(pos.friendId); let chat = this.findChatByFriend(pos.friendId);
if (!chat) return; if (!chat) return;
chat.messages.forEach((m) => { chat.messages.forEach((m) => {
if (m.id && m.selfSend && m.status < MESSAGE_STATUS.RECALL) { if (m.id && m.selfSend && m.status < MESSAGE_STATUS.RECALL) {
@ -92,68 +109,69 @@ export default {
} }
} }
}) })
this.commit("saveToStorage"); this.saveToStorage();
}, },
removeChat(state, idx) { removeChat(idx) {
let chats = this.getters.findChats(); let chats = this.findChats();
if (chats[idx] == state.activeChat) { if (chats[idx] == this.activeChat) {
state.activeChat = null; this.activeChat = null;
} }
chats[idx].delete = true; chats[idx].delete = true;
chats[idx].stored = false; chats[idx].stored = false;
this.commit("saveToStorage"); this.saveToStorage();
}, },
removePrivateChat(state, friendId) { removePrivateChat(friendId) {
let chats = this.getters.findChats(); let chats = this.findChats();
for (let idx in chats) { for (let idx in chats) {
if (chats[idx].type == 'PRIVATE' && if (chats[idx].type == 'PRIVATE' &&
chats[idx].targetId === friendId) { chats[idx].targetId === friendId) {
this.commit("removeChat", idx) this.removeChat(idx);
break; break;
} }
} }
}, },
removeGroupChat(state, groupId) { removeGroupChat(groupId) {
let chats = this.getters.findChats(); let chats = this.findChats();
for (let idx in chats) { for (let idx in chats) {
if (chats[idx].type == 'GROUP' && if (chats[idx].type == 'GROUP' &&
chats[idx].targetId === groupId) { chats[idx].targetId === groupId) {
this.commit("removeChat", idx) this.removeChat(idx);
break; break;
} }
} }
}, },
moveTop(state, idx) { moveTop(idx) {
// 加载中不移动,很耗性能 // 加载中不移动,很耗性能
if (this.getters.isLoading()) { if (this.isLoading()) {
return; return;
} }
if (idx > 0) { if (idx > 0) {
let chats = this.getters.findChats(); let chats = this.findChats();
let chat = chats[idx]; let chat = chats[idx];
chats.splice(idx, 1); chats.splice(idx, 1);
chats.unshift(chat); chats.unshift(chat);
chat.lastSendTime = new Date().getTime(); chat.lastSendTime = new Date().getTime();
chat.stored = false; chat.stored = false;
this.commit("saveToStorage"); this.saveToStorage();
} }
}, },
insertMessage(state, [msgInfo, chatInfo]) { insertMessage(msgInfo, chatInfo) {
let time = new Date().getTime()
let type = chatInfo.type; let type = chatInfo.type;
// 记录消息的最大id // 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) { if (msgInfo.id && type == "PRIVATE" && msgInfo.id > this.privateMsgMaxId) {
state.privateMsgMaxId = msgInfo.id; this.privateMsgMaxId = msgInfo.id;
} }
if (msgInfo.id && type == "GROUP" && msgInfo.id > state.groupMsgMaxId) { if (msgInfo.id && type == "GROUP" && msgInfo.id > this.groupMsgMaxId) {
state.groupMsgMaxId = msgInfo.id; this.groupMsgMaxId = msgInfo.id;
} }
// 如果是已存在消息,则覆盖旧的消息数据 // 如果是已存在消息,则覆盖旧的消息数据
let chat = this.getters.findChat(chatInfo); let chat = this.findChat(chatInfo);
let message = this.getters.findMessage(chat, msgInfo); let message = this.findMessage(chat, msgInfo);
if (message) { if (message) {
Object.assign(message, msgInfo); Object.assign(message, msgInfo);
chat.stored = false; chat.stored = false;
this.commit("saveToStorage"); this.saveToStorage();
return; return;
} }
// 插入新的数据 // 插入新的数据
@ -182,7 +200,8 @@ export default {
// 是否有人@我 // 是否有人@我
if (!msgInfo.selfSend && chat.type == "GROUP" && msgInfo.atUserIds && if (!msgInfo.selfSend && chat.type == "GROUP" && msgInfo.atUserIds &&
msgInfo.status != MESSAGE_STATUS.READED) { msgInfo.status != MESSAGE_STATUS.READED) {
let userId = userStore.state.userInfo.id; let userStore = useUserStore();
let userId = userStore.userInfo.id;
if (msgInfo.atUserIds.indexOf(userId) >= 0) { if (msgInfo.atUserIds.indexOf(userId) >= 0) {
chat.atMe = true; chat.atMe = true;
} }
@ -212,39 +231,44 @@ export default {
} }
chat.messages.splice(insertPos, 0, msgInfo); chat.messages.splice(insertPos, 0, msgInfo);
chat.stored = false; chat.stored = false;
this.commit("saveToStorage"); this.saveToStorage();
console.log("耗时:", new Date().getTime() - time)
}, },
updateMessage(state, [msgInfo, chatInfo]) { updateMessage(msgInfo, chatInfo) {
// 获取对方id或群id // 获取对方id或群id
let chat = this.getters.findChat(chatInfo); let chat = this.findChat(chatInfo);
let message = this.getters.findMessage(chat, msgInfo); let message = this.findMessage(chat, msgInfo);
if (message) { if (message) {
// 属性拷贝 // 属性拷贝
Object.assign(message, msgInfo); Object.assign(message, msgInfo);
chat.stored = false; chat.stored = false;
this.commit("saveToStorage"); this.saveToStorage();
} }
}, },
deleteMessage(state, [msgInfo, chatInfo]) { deleteMessage(msgInfo, chatInfo) {
let chat = this.getters.findChat(chatInfo); let chat = this.findChat(chatInfo);
let isColdMessage = false;
for (let idx in chat.messages) { for (let idx in chat.messages) {
// 已经发送成功的,根据id删除 // 已经发送成功的,根据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); chat.messages.splice(idx, 1);
isColdMessage = idx < chat.hotMinIdx;
break; break;
} }
// 正在发送中的消息可能没有id,只有临时id // 正在发送中的消息可能没有id,只有临时id
if (chat.messages[idx].tmpId && chat.messages[idx].tmpId == msgInfo.tmpId) { if (chat.messages[idx].tmpId && chat.messages[idx].tmpId == msgInfo.tmpId) {
chat.messages.splice(idx, 1); chat.messages.splice(idx, 1);
isColdMessage = idx < chat.hotMinIdx;
break; break;
} }
} }
chat.stored = false; chat.stored = false;
this.commit("saveToStorage"); this.saveToStorage(isColdMessage);
}, },
recallMessage(state, [msgInfo, chatInfo]) { recallMessage(msgInfo, chatInfo) {
let chat = this.getters.findChat(chatInfo); let chat = this.findChat(chatInfo);
if (!chat) return; if (!chat) return;
let isColdMessage = false;
// 要撤回的消息id // 要撤回的消息id
let id = msgInfo.content; let id = msgInfo.content;
let name = msgInfo.selfSend ? '你' : chat.type == 'PRIVATE' ? '对方' : msgInfo.sendNickName; let name = msgInfo.selfSend ? '你' : chat.type == 'PRIVATE' ? '对方' : msgInfo.sendNickName;
@ -262,6 +286,7 @@ export default {
if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) { if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) {
chat.unreadCount++; chat.unreadCount++;
} }
isColdMessage = idx < chat.hotMinIdx;
} }
// 被引用的消息也要撤回 // 被引用的消息也要撤回
if (m.quoteMessage && m.quoteMessage.id == msgInfo.id) { if (m.quoteMessage && m.quoteMessage.id == msgInfo.id) {
@ -271,86 +296,100 @@ export default {
} }
} }
chat.stored = false; chat.stored = false;
this.commit("saveToStorage"); this.saveToStorage(isColdMessage);
}, },
updateChatFromFriend(state, friend) { updateChatFromFriend(friend) {
let chat = this.getters.findChatByFriend(friend.id); let chat = this.findChatByFriend(friend.id);
// 更新会话中的群名和头像 // 更新会话中的群名和头像
if (chat && (chat.headImage != friend.headImage || if (chat && (chat.headImage != friend.headImage ||
chat.showName != friend.nickName)) { chat.showName != friend.nickName)) {
chat.headImage = friend.headImage; chat.headImage = friend.headImage;
chat.showName = friend.nickName; chat.showName = friend.nickName;
chat.stored = false; chat.stored = false;
this.commit("saveToStorage") this.saveToStorage()
} }
}, },
updateChatFromUser(user) { updateChatFromUser(user) {
let chat = this.getters.findChatByFriend(user.id); let chat = this.findChatByFriend(user.id);
// 更新会话中的昵称和头像 // 更新会话中的昵称和头像
if (chat && (chat.headImage != user.headImageThumb || if (chat && (chat.headImage != user.headImageThumb ||
chat.showName != user.nickName)) { chat.showName != user.nickName)) {
chat.headImage = user.headImageThumb; chat.headImage = user.headImageThumb;
chat.showName = user.nickName; chat.showName = user.nickName;
chat.stored = false; chat.stored = false;
this.saveToStorage(); this.saveToStorage();
} }
}, },
updateChatFromGroup(state, group) { updateChatFromGroup(group) {
let chat = this.getters.findChatByGroup(group.id); let chat = this.findChatByGroup(group.id);
if (chat && (chat.headImage != group.headImageThumb || if (chat && (chat.headImage != group.headImageThumb ||
chat.showName != group.showGroupName)) { chat.showName != group.showGroupName)) {
// 更新会话中的群名称和头像 // 更新会话中的群名称和头像
chat.headImage = group.headImageThumb; chat.headImage = group.headImageThumb;
chat.showName = group.showGroupName; chat.showName = group.showGroupName;
chat.stored = false; chat.stored = false;
this.commit("saveToStorage") this.saveToStorage()
} }
}, },
loadingPrivateMsg(state, loading) { setLoadingPrivateMsg(loading) {
state.loadingPrivateMsg = loading; this.loadingPrivateMsg = loading;
if (!this.getters.isLoading()) { if (!this.isLoading()) {
this.commit("refreshChats") this.refreshChats();
} }
}, },
loadingGroupMsg(state, loading) { setLoadingGroupMsg(loading) {
state.loadingGroupMsg = loading; this.loadingGroupMsg = loading;
if (!this.getters.isLoading()) { if (!this.isLoading()) {
this.commit("refreshChats") this.refreshChats();
} }
}, },
refreshChats(state) { refreshChats() {
if (!cacheChats) { if (!cacheChats) return;
return;
}
// 排序 // 排序
cacheChats.sort((chat1, chat2) => { cacheChats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime);
return chat2.lastSendTime - chat1.lastSendTime; // 记录热数据索引位置
}); cacheChats.forEach(chat => chat.hotMinIdx = chat.messages.length);
// 将消息一次性装载回来 // 将消息一次性装载回来
state.chats = cacheChats; this.chats = cacheChats;
// 清空缓存 // 清空缓存
cacheChats = null; cacheChats = null;
this.commit("saveToStorage"); // 持久化消息
this.saveToStorage(true);
}, },
saveToStorage(state) { saveToStorage(withColdMessage) {
// 加载中不保存,防止卡顿 // 加载中不保存,防止卡顿
if (this.getters.isLoading()) { if (this.isLoading()) {
return; return;
} }
let userId = userStore.state.userInfo.id; let userStore = useUserStore();
let userId = userStore.userInfo.id;
let key = "chats-" + userId; let key = "chats-" + userId;
let chatKeys = []; let chatKeys = [];
// 按会话为单位存储, // 按会话为单位存储,
state.chats.forEach((chat) => { this.chats.forEach((chat) => {
// 只存储有改动的会话 // 只存储有改动的会话
let chatKey = `${key}-${chat.type}-${chat.targetId}` let chatKey = `${key}-${chat.type}-${chat.targetId}`
if (!chat.stored) { if (!chat.stored) {
chat.stored = true;
if (chat.delete) { if (chat.delete) {
localForage.removeItem(chatKey); localForage.removeItem(chatKey);
} else { } else {
localForage.setItem(chatKey, chat); // 存储冷数据
if (withColdMessage) {
let coldChat = Object.assign({}, chat);
coldChat.messages = chat.messages.slice(0, chat.hotMinIdx);
localForage.setItem(chatKey, coldChat)
}
// 存储热消息
let hotKey = chatKey + '-hot';
if (chat.messages.length > chat.hotMinIdx) {
let hotChat = Object.assign({}, chat);
hotChat.messages = chat.messages.slice(chat.hotMinIdx)
localForage.setItem(hotKey, hotChat)
} else {
localForage.removeItem(hotKey);
}
} }
chat.stored = true;
} }
if (!chat.delete) { if (!chat.delete) {
chatKeys.push(chatKey); chatKeys.push(chatKey);
@ -358,46 +397,56 @@ export default {
}) })
// 会话核心信息 // 会话核心信息
let chatsData = { let chatsData = {
privateMsgMaxId: state.privateMsgMaxId, privateMsgMaxId: this.privateMsgMaxId,
groupMsgMaxId: state.groupMsgMaxId, groupMsgMaxId: this.groupMsgMaxId,
chatKeys: chatKeys chatKeys: chatKeys
} }
localForage.setItem(key, chatsData) localForage.setItem(key, chatsData)
// 清理已删除的会话 // 清理已删除的会话
state.chats = state.chats.filter(chat => !chat.delete) this.chats = this.chats.filter(chat => !chat.delete)
}, },
clear(state) { clear() {
cacheChats = [] cacheChats = []
state.chats = []; this.chats = [];
state.activeChat = null; this.activeChat = null;
} },
}, loadChat() {
actions: {
loadChat(context) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let userId = userStore.state.userInfo.id; let userStore = useUserStore();
let userId = userStore.userInfo.id;
let key = "chats-" + userId; let key = "chats-" + userId;
localForage.getItem(key).then((chatsData) => { localForage.getItem(key).then((chatsData) => {
if (!chatsData) { if (!chatsData) {
resolve(); resolve();
} else if (chatsData.chats) {
// 兼容旧版本
context.commit("initChats", chatsData);
resolve();
} else if (chatsData.chatKeys) { } else if (chatsData.chatKeys) {
const promises = []; const promises = [];
chatsData.chatKeys.forEach(key => { chatsData.chatKeys.forEach(key => {
promises.push(localForage.getItem(key)) promises.push(localForage.getItem(key))
promises.push(localForage.getItem(key + "-hot"))
}) })
Promise.all(promises).then(chats => { Promise.all(promises).then(chats => {
chatsData.chats = chats.filter(o => o); chatsData.chats = [];
context.commit("initChats", chatsData); // 偶数下标为冷消息,奇数下标为热消息
for (let i = 0; i < chats.length; i += 2) {
if (!chats[i] && !chats[i + 1]) {
continue;
}
let coldChat = chats[i];
let hotChat = chats[i + 1];
// 冷热消息合并
let chat = Object.assign({}, coldChat, hotChat);
if (hotChat && coldChat) {
chat.messages = coldChat.messages.concat(hotChat.messages)
}
chatsData.chats.push(chat);
}
this.initChats(chatsData);
resolve(); resolve();
}) })
} }
}).catch((e) => { }).catch(e => {
console.log("加载消息失败") console.log("加载消息失败")
reject(); reject(e);
}) })
}) })
} }
@ -406,14 +455,14 @@ export default {
isLoading: (state) => () => { isLoading: (state) => () => {
return state.loadingPrivateMsg || state.loadingGroupMsg return state.loadingPrivateMsg || state.loadingGroupMsg
}, },
findChats: (state, getters) => () => { findChats: (state) => () => {
if (cacheChats && getters.isLoading()) { if (cacheChats && state.isLoading()) {
return cacheChats; return cacheChats;
} }
return state.chats; return state.chats;
}, },
findChatIdx: (state, getters) => (chat) => { findChatIdx: (state) => (chat) => {
let chats = getters.findChats(); let chats = state.findChats();
for (let idx in chats) { for (let idx in chats) {
if (chats[idx].type == chat.type && if (chats[idx].type == chat.type &&
chats[idx].targetId === chat.targetId) { chats[idx].targetId === chat.targetId) {
@ -422,18 +471,18 @@ export default {
} }
} }
}, },
findChat: (state, getters) => (chat) => { findChat: (state) => (chat) => {
let chats = getters.findChats(); let chats = state.findChats();
let idx = getters.findChatIdx(chat); let idx = state.findChatIdx(chat);
return chats[idx]; return chats[idx];
}, },
findChatByFriend: (state, getters) => (fid) => { findChatByFriend: (state) => (fid) => {
let chats = getters.findChats(); let chats = state.findChats();
return chats.find(chat => chat.type == 'PRIVATE' && return chats.find(chat => chat.type == 'PRIVATE' &&
chat.targetId == fid) chat.targetId == fid)
}, },
findChatByGroup: (state, getters) => (gid) => { findChatByGroup: (state) => (gid) => {
let chats = getters.findChats(); let chats = state.findChats();
return chats.find(chat => chat.type == 'GROUP' && return chats.find(chat => chat.type == 'GROUP' &&
chat.targetId == gid) chat.targetId == gid)
}, },
@ -454,4 +503,4 @@ export default {
} }
} }
} }
} });

27
im-web/src/store/configStore.js

@ -1,26 +1,24 @@
import { defineStore } from 'pinia';
import http from '../api/httpRequest.js' import http from '../api/httpRequest.js'
export default { export default defineStore('configStore', {
state: { state: () => {
webrtc: {} return {
}, webrtc: {}
mutations: {
setConfig(state, config) {
state.webrtc = config.webrtc;
},
clear(state) {
state.webrtc = {};
} }
}, },
actions: { actions: {
loadConfig(context) { setConfig(config) {
this.webrtc = config.webrtc;
},
loadConfig() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http({ http({
url: '/system/config', url: '/system/config',
method: 'GET' method: 'GET'
}).then((config) => { }).then(config => {
console.log("系统配置", config) console.log("系统配置", config)
context.commit("setConfig", config); this.setConfig(config);
resolve(); resolve();
}).catch((res) => { }).catch((res) => {
reject(res); reject(res);
@ -28,5 +26,4 @@ export default {
}) })
} }
} }
});
}

117
im-web/src/store/friendStore.js

@ -1,101 +1,62 @@
import { defineStore } from 'pinia';
import http from '../api/httpRequest.js' import http from '../api/httpRequest.js'
import { TERMINAL_TYPE } from "../api/enums.js" import { TERMINAL_TYPE } from "../api/enums.js"
export default { export default defineStore('friendStore', {
state: () => {
state: { return {
friends: [], friends: [], // 好友列表
timer: null timer: null
}
}, },
mutations: { actions: {
setFriends(state, friends) { setFriends(friends) {
friends.forEach((f) => { this.friends = friends;
f.online = false;
f.onlineWeb = false;
f.onlineApp = false;
})
state.friends = friends;
}, },
updateFriend(state, friend) { updateFriend(friend) {
state.friends.forEach((f, index) => { this.friends.forEach((f, index) => {
if (f.id == friend.id) { if (f.id == friend.id) {
// 拷贝属性 // 拷贝属性
let online = state.friends[index].online; let online = this.friends[index].online;
Object.assign(state.friends[index], friend); Object.assign(this.friends[index], friend);
state.friends[index].online = online; this.friends[index].online = online;
} }
}) })
}, },
removeFriend(state, id) { removeFriend(id) {
state.friends.filter(f => f.id == id).forEach(f => f.deleted = true); this.friends.filter(f => f.id == id).forEach(f => f.deleted = true);
}, },
addFriend(state, friend) { addFriend(friend) {
if (state.friends.some((f) => f.id == friend.id)) { if (this.friends.some(f => f.id == friend.id)) {
this.commit("updateFriend", friend) this.updateFriend(friend)
} else { } else {
state.friends.unshift(friend); this.friends.unshift(friend);
} }
}, },
refreshOnlineStatus(state) { updateOnlineStatus(onlineData) {
let userIds = state.friends.filter((f) => !f.deleted).map((f) => f.id); let friend = this.findFriend(onlineData.userId);
if (userIds.length == 0) { if (onlineData.terminal == TERMINAL_TYPE.WEB) {
return; friend.onlineWeb = onlineData.online;
} else if (onlineData.terminal == TERMINAL_TYPE.APP) {
friend.onlineApp = onlineData.online;
} }
http({ friend.online = friend.onlineWeb || friend.onlineApp;
url: '/user/terminal/online',
method: 'get',
params: { userIds: userIds.join(',') }
}).then((onlineTerminals) => {
this.commit("setOnlineStatus", onlineTerminals);
})
// 30s后重新拉取
state.timer && clearTimeout(state.timer);
state.timer = setTimeout(() => {
this.commit("refreshOnlineStatus");
}, 30000)
}, },
setOnlineStatus(state, onlineTerminals) { clear() {
state.friends.forEach((f) => { this.timer && clearTimeout(this.timer);
let userTerminal = onlineTerminals.find((o) => f.id == o.userId); this.friends = [];
if (userTerminal) { this.timer = null;
f.online = true;
f.onlineWeb = userTerminal.terminals.indexOf(TERMINAL_TYPE.WEB) >= 0
f.onlineApp = userTerminal.terminals.indexOf(TERMINAL_TYPE.APP) >= 0
} else {
f.online = false;
f.onlineWeb = false;
f.onlineApp = false;
}
});
// 在线的在前面
state.friends.sort((f1, f2) => {
if (f1.online && !f2.online) {
return -1;
}
if (f2.online && !f1.online) {
return 1;
}
return 0;
});
}, },
clear(state) { loadFriend() {
state.timer && clearTimeout(state.timer);
state.friends = [];
state.timer = null;
}
},
actions: {
loadFriend(context) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http({ http({
url: '/friend/list', url: '/friend/list',
method: 'GET' method: 'GET'
}).then((friends) => { }).then(async (friends) => {
context.commit("setFriends", friends); this.setFriends(friends);
context.commit("refreshOnlineStatus"); resolve();
resolve() }).catch(e => {
}).catch(() => { reject(e);
reject();
}) })
}); });
} }
@ -108,4 +69,4 @@ export default {
return state.friends.find((f) => f.id == userId); return state.friends.find((f) => f.id == userId);
} }
} }
} });

66
im-web/src/store/groupStore.js

@ -1,53 +1,63 @@
import { defineStore } from 'pinia';
import http from '../api/httpRequest.js' import http from '../api/httpRequest.js'
export default { export default defineStore('groupStore', {
state: { state: () => {
groups: [] return {
groups: []
}
}, },
mutations: { actions: {
setGroups(state, groups) { setGroups(groups) {
state.groups = groups; this.groups = groups;
}, },
addGroup(state, group) { addGroup(group) {
if (state.groups.some((g) => g.id == group.id)) { if (this.groups.some(g => g.id == group.id)) {
this.commit("updateGroup", group) this.updateGroup(group)
} else { } else {
state.groups.unshift(group); this.groups.unshift(group);
} }
}, },
removeGroup(state, id) { removeGroup(id) {
state.groups.filter(g => g.id == id).forEach(g => g.quit = true); this.groups.filter(g => g.id == id).forEach(g => g.quit = true);
}, },
updateGroup(state, group) { updateGroup(group) {
state.groups.forEach((g, idx) => { this.groups.forEach((g, idx) => {
if (g.id == group.id) { if (g.id == group.id) {
// 拷贝属性 // 拷贝属性
Object.assign(state.groups[idx], group); Object.assign(this.groups[idx], group);
} }
}) })
}, },
clear(state) { updateTopMessage(id, topMessage) {
state.groups = []; let group = this.findGroup(id);
} if (group) {
}, group.topMessage = topMessage;
actions: { }
loadGroup(context) { },
clear() {
this.groups = [];
},
loadGroup() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http({ http({
url: '/group/list', url: '/group/list',
method: 'GET' method: 'GET'
}).then((groups) => { }).then(groups => {
context.commit("setGroups", groups); this.setGroups(groups);
resolve(); resolve();
}).catch((res) => { }).catch(e => {
reject(res); reject(e);
}) })
}); });
} }
}, },
getters: { getters: {
findGroup: (state) => (id) => { findGroup: (state) => (id) => {
return state.groups.find((g) => g.id == id); return state.groups.find(g => g.id == id);
} },
isGroup: (state) => (id) => {
return state.groups.filter(g => !g.quit).some(g => g.id == id);
},
} }
} });

33
im-web/src/store/index.js

@ -1,33 +0,0 @@
import Vue from 'vue';
import Vuex from 'vuex';
import chatStore from './chatStore.js';
import friendStore from './friendStore.js';
import userStore from './userStore.js';
import groupStore from './groupStore.js';
import configStore from './configStore.js';
import uiStore from './uiStore.js';
Vue.use(Vuex)
export default new Vuex.Store({
modules: { chatStore, friendStore, userStore, groupStore, configStore, uiStore },
state: {},
mutations: {
},
actions: {
load(context) {
return this.dispatch("loadUser").then(() => {
const promises = [];
promises.push(this.dispatch("loadFriend"));
promises.push(this.dispatch("loadGroup"));
promises.push(this.dispatch("loadChat"));
promises.push(this.dispatch("loadConfig"));
return Promise.all(promises);
})
},
unload(context) {
context.commit("clear");
}
},
strict: process.env.NODE_ENV !== 'production'
})

39
im-web/src/store/uiStore.js

@ -1,39 +0,0 @@
export default {
state: {
userInfo: { // 用户信息窗口
show: false,
user: {},
pos: {
x: 0,
y: 0
}
},
fullImage: { // 全屏大图
show: false,
url: ""
}
},
mutations: {
showUserInfoBox(state, user) {
state.userInfo.show = true;
state.userInfo.user = user;
},
setUserInfoBoxPos(state, pos) {
let w = document.documentElement.clientWidth;
let h = document.documentElement.clientHeight;
state.userInfo.pos.x = Math.min(pos.x, w - 350);
state.userInfo.pos.y = Math.min(pos.y, h - 200);
},
closeUserInfoBox(state) {
state.userInfo.show = false;
},
showFullImageBox(state, url) {
state.fullImage.show = true;
state.fullImage.url = url;
},
closeFullImageBox(state) {
state.fullImage.show = false;
}
}
}

50
im-web/src/store/userStore.js

@ -1,45 +1,43 @@
import { defineStore } from 'pinia';
import http from '../api/httpRequest.js' import http from '../api/httpRequest.js'
import { RTC_STATE } from "../api/enums.js" import { RTC_STATE } from "../api/enums.js"
export default {
state: { export default defineStore('userStore', {
userInfo: { state: () => {
return {
}, userInfo: {},
rtcInfo: { rtcInfo: {
friend: {}, // 好友信息 friend: {}, // 好友信息
mode: "video", // 模式 video:视频 voice:语音 mode: "video", // 模式 video:视频 voice:语音
state: RTC_STATE.FREE // FREE:空闲 WAIT_CALL:呼叫方等待 WAIT_ACCEPT: 被呼叫方等待接听 CHATING:聊天中 state: RTC_STATE.FREE // FREE:空闲 WAIT_CALL:呼叫方等待 WAIT_ACCEPT: 被呼叫方等待接听 CHATING:聊天中
}
} }
}, },
actions: {
mutations: { setUserInfo(userInfo) {
setUserInfo(state, userInfo) { this.userInfo = userInfo
state.userInfo = userInfo
}, },
setRtcInfo(state, rtcInfo) { setRtcInfo(rtcInfo) {
state.rtcInfo = rtcInfo; this.rtcInfo = rtcInfo;
}, },
setRtcState(state, rtcState) { setRtcState(rtcState) {
state.rtcInfo.state = rtcState; this.rtcInfo.state = rtcState;
}, },
clear(state) { clear() {
state.userInfo = {}; this.userInfo = {};
state.rtcInfo = { this.rtcInfo = {
friend: {}, friend: {},
mode: "video", mode: "video",
state: RTC_STATE.FREE state: RTC_STATE.FREE
}; };
} },
}, loadUser() {
actions: {
loadUser(context) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
http({ http({
url: '/user/self', url: '/user/self',
method: 'GET' method: 'GET'
}).then((userInfo) => { }).then((userInfo) => {
context.commit("setUserInfo", userInfo); this.setUserInfo(userInfo);
resolve(); resolve();
}).catch((res) => { }).catch((res) => {
reject(res); reject(res);
@ -47,4 +45,4 @@ export default {
}) })
} }
} }
} });

9
im-web/src/view/Chat.vue

@ -43,19 +43,16 @@ export default {
}, },
methods: { methods: {
onActiveItem(index) { onActiveItem(index) {
this.$store.commit("activeChat", index); this.chatStore.setActiveChat(index);
}, },
onDelItem(index) { onDelItem(index) {
this.$store.commit("removeChat", index); this.chatStore.removeChat(index);
}, },
onTop(chatIdx) { onTop(chatIdx) {
this.$store.commit("moveTop", chatIdx); this.chatStore.moveTop(chatIdx);
}, },
}, },
computed: { computed: {
chatStore() {
return this.$store.state.chatStore;
},
loading() { loading() {
return this.chatStore.loadingGroupMsg || this.chatStore.loadingPrivateMsg return this.chatStore.loadingGroupMsg || this.chatStore.loadingPrivateMsg
} }

21
im-web/src/view/Friend.vue

@ -101,8 +101,8 @@ export default {
method: 'delete' method: 'delete'
}).then(() => { }).then(() => {
this.$message.success("删除好友成功"); this.$message.success("删除好友成功");
this.$store.commit("removeFriend", friend.id); this.friendStore.removeFriend(friend.id);
this.$store.commit("removePrivateChat", friend.id); this.chatStore.removePrivateChat(friend.id);
}) })
}) })
}, },
@ -121,7 +121,7 @@ export default {
headImage: user.headImage, headImage: user.headImage,
online: user.online online: user.online
} }
this.$store.commit("addFriend", friend); this.friendStore.addFriend(friend);
}) })
}, },
onSendMessage(user) { onSendMessage(user) {
@ -131,13 +131,13 @@ export default {
showName: user.nickName, showName: user.nickName,
headImage: user.headImageThumb, headImage: user.headImageThumb,
}; };
this.$store.commit("openChat", chat); this.chatStore.openChat(chat);
this.$store.commit("activeChat", 0); this.chatStore.setActiveChat(0);
this.$router.push("/home/chat"); this.$router.push("/home/chat");
}, },
showFullImage() { showFullImage() {
if (this.userInfo.headImage) { if (this.userInfo.headImage) {
this.$store.commit('showFullImageBox', this.userInfo.headImage); this.$eventBus.$emit("openFullImage", this.userInfo.headImage);
} }
}, },
updateFriendInfo() { updateFriendInfo() {
@ -146,8 +146,8 @@ export default {
let friend = JSON.parse(JSON.stringify(this.activeFriend)); let friend = JSON.parse(JSON.stringify(this.activeFriend));
friend.headImage = this.userInfo.headImageThumb; friend.headImage = this.userInfo.headImageThumb;
friend.nickName = this.userInfo.nickName; friend.nickName = this.userInfo.nickName;
this.$store.commit("updateChatFromFriend", friend); this.chatStore.updateChatFromFriend(friend);
this.$store.commit("updateFriend", friend); this.friendStore.updateFriend(friend);
} }
}, },
loadUserInfo(id) { loadUserInfo(id) {
@ -174,11 +174,8 @@ export default {
} }
}, },
computed: { computed: {
friendStore() {
return this.$store.state.friendStore;
},
isFriend() { isFriend() {
return this.$store.getters.isFriend(this.userInfo.id); return this.friendStore.isFriend(this.userInfo.id);
}, },
friendMap() { friendMap() {
// //

35
im-web/src/view/Group.vue

@ -31,7 +31,7 @@
<i v-else class="el-icon-plus avatar-uploader-icon"></i> <i v-else class="el-icon-plus avatar-uploader-icon"></i>
</file-upload> </file-upload>
<head-image v-show="!isOwner" class="avatar" :size="160" :url="activeGroup.headImage" <head-image v-show="!isOwner" class="avatar" :size="160" :url="activeGroup.headImage"
:name="activeGroup.showGroupName" radius="10%"> :name="activeGroup.showGroupName" radius="10%" @click.native="showFullImage()">
</head-image> </head-image>
<el-button class="send-btn" icon="el-icon-position" type="primary" @click="onSendMessage()">发消息 <el-button class="send-btn" icon="el-icon-position" type="primary" @click="onSendMessage()">发消息
</el-button> </el-button>
@ -50,7 +50,7 @@
</el-form-item> </el-form-item>
<el-form-item label="我在本群的昵称"> <el-form-item label="我在本群的昵称">
<el-input v-model="activeGroup.remarkNickName" maxlength="20" <el-input v-model="activeGroup.remarkNickName" maxlength="20"
:placeholder="$store.state.userStore.userInfo.nickName"></el-input> :placeholder="userStore.userInfo.nickName"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="群公告"> <el-form-item label="群公告">
<el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" :rows="3" <el-input v-model="activeGroup.notice" :disabled="!isOwner" type="textarea" :rows="3"
@ -137,8 +137,7 @@ export default {
cancelButtonText: '取消', cancelButtonText: '取消',
inputPattern: /\S/, inputPattern: /\S/,
inputErrorMessage: '请输入群聊名称' inputErrorMessage: '请输入群聊名称'
}).then((o) => { }).then(o => {
let userInfo = this.$store.state.userStore.userInfo;
let data = { let data = {
name: o.value name: o.value
} }
@ -147,7 +146,9 @@ export default {
method: 'post', method: 'post',
data: data data: data
}).then((group) => { }).then((group) => {
this.$store.commit("addGroup", group); this.groupStore.addGroup(group);
this.onActiveItem(group)
this.$message.success('创建成功');
}) })
}) })
}, },
@ -195,7 +196,7 @@ export default {
method: "put", method: "put",
data: vo data: vo
}).then((group) => { }).then((group) => {
this.$store.commit("updateGroup", group); this.groupStore.updateGroup(group);
this.$message.success("修改成功"); this.$message.success("修改成功");
}) })
} }
@ -212,7 +213,7 @@ export default {
method: 'delete' method: 'delete'
}).then(() => { }).then(() => {
this.$message.success(`群聊'${this.activeGroup.name}'已解散`); this.$message.success(`群聊'${this.activeGroup.name}'已解散`);
this.$store.commit("removeGroup", this.activeGroup.id); this.groupStore.removeGroup(this.activeGroup.id);
this.reset(); this.reset();
}); });
}) })
@ -228,8 +229,8 @@ export default {
method: 'delete' method: 'delete'
}).then(() => { }).then(() => {
this.$message.success(`您已退出'${this.activeGroup.name}'`); this.$message.success(`您已退出'${this.activeGroup.name}'`);
this.$store.commit("removeGroup", this.activeGroup.id); this.groupStore.removeGroup(this.activeGroup.id);
this.$store.commit("removeGroupChat", this.activeGroup.id); this.chatStore.removeGroupChat(this.activeGroup.id);
this.reset(); this.reset();
}); });
}) })
@ -241,8 +242,8 @@ export default {
showName: this.activeGroup.showGroupName, showName: this.activeGroup.showGroupName,
headImage: this.activeGroup.headImage, headImage: this.activeGroup.headImage,
}; };
this.$store.commit("openChat", chat); this.chatStore.openChat(chat);
this.$store.commit("activeChat", 0); this.chatStore.setActiveChat(0);
this.$router.push("/home/chat"); this.$router.push("/home/chat");
}, },
onScroll(e) { onScroll(e) {
@ -254,6 +255,11 @@ export default {
} }
} }
}, },
showFullImage() {
if (this.activeGroup.headImage) {
this.$eventBus.$emit("openFullImage", this.activeGroup.headImage);
}
},
loadGroupMembers() { loadGroupMembers() {
this.$http({ this.$http({
url: `/group/members/${this.activeGroup.id}`, url: `/group/members/${this.activeGroup.id}`,
@ -280,15 +286,12 @@ export default {
} }
}, },
computed: { computed: {
groupStore() {
return this.$store.state.groupStore;
},
ownerName() { ownerName() {
let member = this.groupMembers.find((m) => m.userId == this.activeGroup.ownerId); let member = this.groupMembers.find(m => m.userId == this.activeGroup.ownerId);
return member && member.showNickName; return member && member.showNickName;
}, },
isOwner() { isOwner() {
return this.activeGroup.ownerId == this.$store.state.userStore.userInfo.id; return this.activeGroup.ownerId == this.userStore.userInfo.id;
}, },
imageAction() { imageAction() {
return `/image/upload`; return `/image/upload`;

116
im-web/src/view/Home.vue

@ -1,16 +1,14 @@
<template> <template>
<div class="home-page" @click="$store.commit('closeUserInfoBox')"> <div class="home-page" @click="closeUserInfo">
<div class="app-container" :class="{ fullscreen: isFullscreen }"> <div class="app-container" :class="{ fullscreen: isFullscreen }">
<div class="navi-bar"> <div class="navi-bar">
<div class="navi-bar-box"> <div class="navi-bar-box">
<div class="top"> <div class="top">
<div class="user-head-image"> <div class="user-head-image">
<head-image :name="$store.state.userStore.userInfo.nickName" :size="38" <head-image :name="userStore.userInfo.nickName" :size="38"
:url="$store.state.userStore.userInfo.headImageThumb" :url="userStore.userInfo.headImageThumb" @click.native="showSettingDialog = true">
@click.native="showSettingDialog = true">
</head-image> </head-image>
</div> </div>
<div class="menu"> <div class="menu">
<router-link class="link" v-bind:to="'/home/chat'"> <router-link class="link" v-bind:to="'/home/chat'">
<div class="menu-item"> <div class="menu-item">
@ -48,10 +46,8 @@
<router-view></router-view> <router-view></router-view>
</div> </div>
<setting :visible="showSettingDialog" @close="closeSetting()"></setting> <setting :visible="showSettingDialog" @close="closeSetting()"></setting>
<user-info v-show="uiStore.userInfo.show" :pos="uiStore.userInfo.pos" :user="uiStore.userInfo.user" <user-info ref="userInfo"></user-info>
@close="$store.commit('closeUserInfoBox')"></user-info> <full-image ref="fullImage"></full-image>
<full-image :visible="uiStore.fullImage.show" :url="uiStore.fullImage.url"
@close="$store.commit('closeFullImageBox')"></full-image>
<rtc-private-video ref="rtcPrivateVideo"></rtc-private-video> <rtc-private-video ref="rtcPrivateVideo"></rtc-private-video>
<rtc-group-video ref="rtcGroupVideo"></rtc-group-video> <rtc-group-video ref="rtcGroupVideo"></rtc-group-video>
</div> </div>
@ -95,8 +91,15 @@ export default {
// //
this.$refs.rtcGroupVideo.open(rctInfo); this.$refs.rtcGroupVideo.open(rctInfo);
}); });
this.$eventBus.$on('openUserInfo', (user, pos) => {
this.$store.dispatch("load").then(() => { //
this.$refs.userInfo.open(user, pos);
});
this.$eventBus.$on('openFullImage', url => {
//
this.$refs.fullImage.open(url);
});
this.loadStore().then(() => {
// ws // ws
this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken")); this.$wsApi.connect(process.env.VUE_APP_WS_URL, sessionStorage.getItem("accessToken"));
this.$wsApi.onConnect(() => { this.$wsApi.onConnect(() => {
@ -104,8 +107,8 @@ export default {
this.onReconnectWs(); this.onReconnectWs();
} else { } else {
// 线 // 线
this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId); this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId); this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId);
} }
}); });
this.$wsApi.onMessage((cmd, msgInfo) => { this.$wsApi.onMessage((cmd, msgInfo) => {
@ -145,7 +148,7 @@ export default {
// //
this.reconnecting = true; this.reconnecting = true;
// token // token
this.$store.dispatch("loadUser").then(() => { this.userStore.loadUser().then(() => {
// 线 // 线
this.$message.error("连接断开,正在尝试重新连接..."); this.$message.error("连接断开,正在尝试重新连接...");
this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem( this.$wsApi.reconnect(process.env.VUE_APP_WS_URL, sessionStorage.getItem(
@ -160,39 +163,55 @@ export default {
this.reconnecting = false; this.reconnecting = false;
// //
const promises = []; const promises = [];
promises.push(this.$store.dispatch("loadFriend")); promises.push(this.friendStore.loadFriend());
promises.push(this.$store.dispatch("loadGroup")); promises.push(this.groupStore.loadGroup());
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
// 线 // 线
this.pullPrivateOfflineMessage(this.$store.state.chatStore.privateMsgMaxId); this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId);
this.pullGroupOfflineMessage(this.$store.state.chatStore.groupMsgMaxId); this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId);
this.$message.success("重新连接成功"); this.$message.success("重新连接成功");
}).catch(() => { }).catch(() => {
this.$message.error("初始化失败"); this.$message.error("初始化失败");
this.onExit(); this.onExit();
}) })
}, },
loadStore() {
return this.userStore.loadUser().then(() => {
const promises = [];
promises.push(this.friendStore.loadFriend());
promises.push(this.groupStore.loadGroup());
promises.push(this.chatStore.loadChat());
promises.push(this.configStore.loadConfig());
return Promise.all(promises);
})
},
unloadStore() {
this.friendStore.clear();
this.groupStore.clear();
this.chatStore.clear();
this.userStore.clear();
},
pullPrivateOfflineMessage(minId) { pullPrivateOfflineMessage(minId) {
this.$store.commit("loadingPrivateMsg", true) this.chatStore.setLoadingPrivateMsg(true)
this.$http({ this.$http({
url: "/message/private/pullOfflineMessage?minId=" + minId, url: "/message/private/pullOfflineMessage?minId=" + minId,
method: 'GET' method: 'GET'
}).catch(() => { }).catch(() => {
this.$store.commit("loadingPrivateMsg", false) this.chatStore.setLoadingPrivateMsg(false)
}) })
}, },
pullGroupOfflineMessage(minId) { pullGroupOfflineMessage(minId) {
this.$store.commit("loadingGroupMsg", true) this.chatStore.setLoadingGroupMsg(true)
this.$http({ this.$http({
url: "/message/group/pullOfflineMessage?minId=" + minId, url: "/message/group/pullOfflineMessage?minId=" + minId,
method: 'GET' method: 'GET'
}).catch(() => { }).catch(() => {
this.$store.commit("loadingGroupMsg", false) this.chatStore.setLoadingGroupMsg(false)
}) })
}, },
handlePrivateMessage(msg) { handlePrivateMessage(msg) {
// //
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id; msg.selfSend = msg.sendId == this.userStore.userInfo.id;
// id // id
let friendId = msg.selfSend ? msg.recvId : msg.sendId; let friendId = msg.selfSend ? msg.recvId : msg.sendId;
// //
@ -202,34 +221,34 @@ export default {
} }
// //
if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) { if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) {
this.$store.commit("loadingPrivateMsg", JSON.parse(msg.content)) this.chatStore.setLoadingPrivateMsg(JSON.parse(msg.content))
return; return;
} }
// //
if (msg.type == this.$enums.MESSAGE_TYPE.READED) { if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
this.$store.commit("resetUnreadCount", chatInfo) this.chatStore.resetUnreadCount(chatInfo)
return; return;
} }
// , // ,
if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) { if (msg.type == this.$enums.MESSAGE_TYPE.RECEIPT) {
this.$store.commit("readedMessage", { this.chatStore.readedMessage({
friendId: msg.sendId friendId: msg.sendId
}) })
return; return;
} }
// //
if (msg.type == this.$enums.MESSAGE_TYPE.RECALL) { if (msg.type == this.$enums.MESSAGE_TYPE.RECALL) {
this.$store.commit("recallMessage", [msg, chatInfo]) this.chatStore.recallMessage(msg, chatInfo)
return; return;
} }
// //
if (msg.type == this.$enums.MESSAGE_TYPE.FRIEND_NEW) { if (msg.type == this.$enums.MESSAGE_TYPE.FRIEND_NEW) {
this.$store.commit("addFriend", JSON.parse(msg.content)); this.friendStore.addFriend(JSON.parse(msg.content));
return; return;
} }
// //
if (msg.type == this.$enums.MESSAGE_TYPE.FRIEND_DEL) { if (msg.type == this.$enums.MESSAGE_TYPE.FRIEND_DEL) {
this.$store.commit("removeFriend", friendId); this.friendStore.removeFriend(friendId);
return; return;
} }
// webrtc // webrtc
@ -251,9 +270,9 @@ export default {
headImage: friend.headImage headImage: friend.headImage
}; };
// //
this.$store.commit("openChat", chatInfo); this.chatStore.openChat(chatInfo);
// //
this.$store.commit("insertMessage", [msg, chatInfo]); this.chatStore.insertMessage(msg, chatInfo);
// //
if (!msg.selfSend && this.$msgType.isNormal(msg.type) && if (!msg.selfSend && this.$msgType.isNormal(msg.type) &&
msg.status != this.$enums.MESSAGE_STATUS.READED) { msg.status != this.$enums.MESSAGE_STATUS.READED) {
@ -262,20 +281,20 @@ export default {
}, },
handleGroupMessage(msg) { handleGroupMessage(msg) {
// //
msg.selfSend = msg.sendId == this.$store.state.userStore.userInfo.id; msg.selfSend = msg.sendId == this.userStore.userInfo.id;
let chatInfo = { let chatInfo = {
type: 'GROUP', type: 'GROUP',
targetId: msg.groupId targetId: msg.groupId
} }
// //
if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) { if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) {
this.$store.commit("loadingGroupMsg", JSON.parse(msg.content)) this.chatStore.setLoadingGroupMsg(JSON.parse(msg.content))
return; return;
} }
// //
if (msg.type == this.$enums.MESSAGE_TYPE.READED) { if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
// //
this.$store.commit("resetUnreadCount", chatInfo) this.chatStore.resetUnreadCount(chatInfo)
return; return;
} }
// //
@ -287,22 +306,22 @@ export default {
readedCount: msg.readedCount, readedCount: msg.readedCount,
receiptOk: msg.receiptOk receiptOk: msg.receiptOk
}; };
this.$store.commit("updateMessage", [msgInfo, chatInfo]) this.chatStore.updateMessage(msgInfo, chatInfo)
return; return;
} }
// //
if (msg.type == this.$enums.MESSAGE_TYPE.RECALL) { if (msg.type == this.$enums.MESSAGE_TYPE.RECALL) {
this.$store.commit("recallMessage", [msg, chatInfo]) this.chatStore.recallMessage(msg, chatInfo)
return; return;
} }
// //
if (msg.type == this.$enums.MESSAGE_TYPE.GROUP_NEW) { if (msg.type == this.$enums.MESSAGE_TYPE.GROUP_NEW) {
this.$store.commit("addGroup", JSON.parse(msg.content)); this.groupStore.addGroup(JSON.parse(msg.content));
return; return;
} }
// //
if (msg.type == this.$enums.MESSAGE_TYPE.GROUP_DEL) { if (msg.type == this.$enums.MESSAGE_TYPE.GROUP_DEL) {
this.$store.commit("removeGroup", msg.groupId); this.groupStore.removeGroup(msg.groupId);
return; return;
} }
// //
@ -326,9 +345,9 @@ export default {
headImage: group.headImageThumb headImage: group.headImageThumb
}; };
// //
this.$store.commit("openChat", chatInfo); this.chatStore.openChat(chatInfo);
// //
this.$store.commit("insertMessage", [msg, chatInfo]); this.chatStore.insertMessage(msg, chatInfo);
// //
if (!msg.selfSend && msg.type <= this.$enums.MESSAGE_TYPE.VIDEO && if (!msg.selfSend && msg.type <= this.$enums.MESSAGE_TYPE.VIDEO &&
msg.status != this.$enums.MESSAGE_STATUS.READED) { msg.status != this.$enums.MESSAGE_STATUS.READED) {
@ -341,21 +360,25 @@ export default {
this.$wsApi.close(3000); this.$wsApi.close(3000);
this.$alert("您的账号已被管理员封禁,原因:" + msg.content, "账号被封禁", { this.$alert("您的账号已被管理员封禁,原因:" + msg.content, "账号被封禁", {
confirmButtonText: '确定', confirmButtonText: '确定',
callback: action => { callback: () => {
this.onExit(); this.onExit();
} }
}); });
return; return;
} }
}, },
closeUserInfo() {
this.$refs.userInfo.close();
},
onExit() { onExit() {
this.unloadStore();
this.$wsApi.close(3000); this.$wsApi.close(3000);
sessionStorage.removeItem("accessToken"); sessionStorage.removeItem("accessToken");
location.href = "/"; location.href = "/";
}, },
playAudioTip() { playAudioTip() {
// 线 // 线
if (this.$store.getters.isLoading()) { if (this.chatStore.isLoading()) {
return; return;
} }
// //
@ -375,7 +398,7 @@ export default {
this.showSettingDialog = false; this.showSettingDialog = false;
}, },
loadFriendInfo(id) { loadFriendInfo(id) {
let friend = this.$store.getters.findFriend(id); let friend = this.friendStore.findFriend(id);
if (!friend) { if (!friend) {
friend = { friend = {
id: id, id: id,
@ -386,7 +409,7 @@ export default {
return friend; return friend;
}, },
loadGroupInfo(id) { loadGroupInfo(id) {
let group = this.$store.getters.findGroup(id); let group = this.groupStore.findGroup(id);
if (!group) { if (!group) {
group = { group = {
id: id, id: id,
@ -398,12 +421,9 @@ export default {
} }
}, },
computed: { computed: {
uiStore() {
return this.$store.state.uiStore;
},
unreadCount() { unreadCount() {
let unreadCount = 0; let unreadCount = 0;
let chats = this.$store.state.chatStore.chats; let chats = this.chatStore.chats;
chats.forEach((chat) => { chats.forEach((chat) => {
if (!chat.delete) { if (!chat.delete) {
unreadCount += chat.unreadCount unreadCount += chat.unreadCount

Loading…
Cancel
Save