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. 3
      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.IMUserInfo;
import com.bx.imcommon.util.CommaTextUtils;
import com.bx.imcommon.util.ThreadPoolExecutorFactory;
import com.bx.implatform.contant.Constant;
import com.bx.implatform.contant.RedisKey;
import com.bx.implatform.dto.GroupMessageDTO;
@ -40,19 +41,21 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, GroupMessage> implements
GroupMessageService {
public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, GroupMessage>
implements GroupMessageService {
private final GroupService groupService;
private final GroupMemberService groupMemberService;
private final RedisTemplate<String, Object> redisTemplate;
private final IMClient imClient;
private final SensitiveFilterUtil sensitiveFilterUtil;
private static final ScheduledThreadPoolExecutor EXECUTOR = ThreadPoolExecutorFactory.getThreadPoolExecutor();
@Override
public GroupMessageVO sendMessage(GroupMessageDTO dto) {
@ -67,7 +70,8 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
List<Long> userIds = groupMemberService.findUserIdsByGroupId(group.getId());
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());
@ -78,11 +82,10 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
msg.setSendNickName(member.getShowNickName());
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()));
}
this.save(msg);
// 群发
GroupMessageVO msgInfo = BeanUtils.copyProperties(msg, GroupMessageVO.class);
msgInfo.setAtUserIds(dto.getAtUserIds());
@ -140,97 +143,101 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
return msgInfo;
}
@Override
public void pullOfflineMessage(Long minId) {
UserSession session = SessionContext.getSession();
if(!imClient.isOnline(session.getUserId())){
if (!imClient.isOnline(session.getUserId())) {
throw new GlobalException("网络连接失败,无法拉取离线消息");
}
// 查询用户加入的群组
List<GroupMember> members = groupMemberService.findByUserId(session.getUserId());
Map<Long, GroupMember> groupMemberMap = CollStreamUtil.toIdentityMap(members, GroupMember::getGroupId);
Set<Long> groupIds = groupMemberMap.keySet();
if(CollectionUtil.isEmpty(groupIds)){
if (CollectionUtil.isEmpty(groupIds)) {
// 关闭加载中标志
this.sendLoadingMessage(false);
this.sendLoadingMessage(false, session);
return;
}
// 开启加载中标志
this.sendLoadingMessage(true);
// 只能拉取最近3个月的,最多拉取3000条
// 只能拉取最近3个月的,移动端只拉最近一个月
int months = session.getTerminal().equals(IMTerminalType.APP.code()) ? 1 : 3;
Date minDate = DateUtils.addMonths(new Date(), -months);
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.gt(GroupMessage::getId, minId)
.gt(GroupMessage::getSendTime, minDate)
.in(GroupMessage::getGroupId, groupIds)
.orderByAsc(GroupMessage::getId);
wrapper.gt(GroupMessage::getId, minId).gt(GroupMessage::getSendTime, minDate)
.in(GroupMessage::getGroupId, groupIds).orderByAsc(GroupMessage::getId);
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());
for(GroupMember quitMember: quitMembers){
for (GroupMember quitMember : quitMembers) {
wrapper = Wrappers.lambdaQuery();
wrapper.gt(GroupMessage::getId, minId)
.between(GroupMessage::getSendTime, minDate,quitMember.getQuitTime())
wrapper.gt(GroupMessage::getId, minId).between(GroupMessage::getSendTime, minDate, quitMember.getQuitTime())
.eq(GroupMessage::getGroupId, quitMember.getGroupId())
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code())
.orderByAsc(GroupMessage::getId);
.ne(GroupMessage::getStatus, MessageStatus.RECALL.code()).orderByAsc(GroupMessage::getId);
List<GroupMessage> groupMessages = this.list(wrapper);
messageGroupMap.put(quitMember.getGroupId(),groupMessages);
groupMemberMap.put(quitMember.getGroupId(),quitMember);
messageGroupMap.put(quitMember.getGroupId(), groupMessages);
groupMemberMap.put(quitMember.getGroupId(), quitMember);
}
// 推送消息
AtomicInteger sendCount = new AtomicInteger();
messageGroupMap.forEach((groupId, groupMessages) -> {
// 填充消息状态
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
Object o = redisTemplate.opsForHash().get(key, session.getUserId().toString());
long readedMaxId = Objects.isNull(o) ? -1 : Long.parseLong(o.toString());
Map<Object, Object> maxIdMap = null;
for(GroupMessage m:groupMessages){
// 排除加群之前的消息
GroupMember member = groupMemberMap.get(m.getGroupId());
if(DateUtil.compare(member.getCreatedTime(), m.getSendTime()) > 0){
continue;
EXECUTOR.execute(() -> {
// 开启加载中标志
this.sendLoadingMessage(true, session);
// 推送消息
AtomicInteger sendCount = new AtomicInteger();
messageGroupMap.forEach((groupId, groupMessages) -> {
// 第一次拉取时,一个群最多推送1w条消息,防止前端接收能力溢出导致卡顿
List<GroupMessage> sendMessages = groupMessages;
if (minId <= 0 && groupMessages.size() > 10000) {
sendMessages = groupMessages.subList(groupMessages.size() - 10000, groupMessages.size());
}
// 排除不需要接收的消息
List<String> recvIds = CommaTextUtils.asList(m.getRecvIds());
if(!recvIds.isEmpty() && !recvIds.contains(session.getUserId().toString())){
continue;
}
// 组装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);
// 填充消息状态
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
Object o = redisTemplate.opsForHash().get(key, session.getUserId().toString());
long readedMaxId = Objects.isNull(o) ? -1 : Long.parseLong(o.toString());
Map<Object, Object> maxIdMap = null;
for (GroupMessage m : sendMessages) {
// 排除加群之前的消息
GroupMember member = groupMemberMap.get(m.getGroupId());
if (DateUtil.compare(member.getCreatedTime(), m.getSendTime()) > 0) {
continue;
}
// 排除不需要接收的消息
List<String> recvIds = CommaTextUtils.asList(m.getRecvIds());
if (!recvIds.isEmpty() && !recvIds.contains(session.getUserId().toString())) {
continue;
}
// 组装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()));
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();
}
});
// 关闭加载中标志
this.sendLoadingMessage(false, session);
log.info("拉取离线群聊消息,用户id:{},数量:{}", session.getUserId(), sendCount.get());
});
// 关闭加载中标志
this.sendLoadingMessage(false);
log.info("拉取离线群聊消息,用户id:{},数量:{}",session.getUserId(),sendCount.get());
}
@Override
@ -238,10 +245,8 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
UserSession session = SessionContext.getSession();
// 取出最后的消息id
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.eq(GroupMessage::getGroupId, groupId)
.orderByDesc(GroupMessage::getId)
.last("limit 1")
.select(GroupMessage::getId);
wrapper.eq(GroupMessage::getGroupId, groupId).orderByDesc(GroupMessage::getId).last("limit 1")
.select(GroupMessage::getId);
GroupMessage message = this.getOne(wrapper);
if (Objects.isNull(message)) {
return;
@ -276,9 +281,10 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
List<Long> userIds = groupMemberService.findUserIdsByGroupId(groupId);
Map<Object, Object> maxIdMap = redisTemplate.opsForHash().entries(key);
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);
this.updateById(receiptMessage);
}
@ -311,12 +317,12 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
if (Objects.isNull(member) || member.getQuit()) {
throw new GlobalException("您已不在群聊里面");
}
// 已读位置key
// 已读位置key
String key = StrUtil.join(":", RedisKey.IM_GROUP_READED_POSITION, groupId);
// 一次获取所有用户的已读位置
Map<Object, Object> maxIdMap = redisTemplate.opsForHash().entries(key);
// 返回已读用户的id集合
return getReadedUserIds(maxIdMap, message.getId(),message.getSendId());
return getReadedUserIds(maxIdMap, message.getId(), message.getSendId());
}
@Override
@ -333,10 +339,11 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
// 查询聊天记录,只查询加入群聊时间之后的消息
QueryWrapper<GroupMessage> wrapper = new QueryWrapper<>();
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<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());
return messageInfos;
}
@ -354,8 +361,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
return userIds;
}
private void sendLoadingMessage(Boolean isLoadding){
UserSession session = SessionContext.getSession();
private void sendLoadingMessage(Boolean isLoadding, UserSession session) {
GroupMessageVO msgInfo = new GroupMessageVO();
msgInfo.setType(MessageType.LOADING.code());
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.model.IMPrivateMessage;
import com.bx.imcommon.model.IMUserInfo;
import com.bx.imcommon.util.ThreadPoolExecutorFactory;
import com.bx.implatform.dto.PrivateMessageDTO;
import com.bx.implatform.entity.PrivateMessage;
import com.bx.implatform.enums.MessageStatus;
@ -32,6 +33,7 @@ import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.stream.Collectors;
@Slf4j
@ -43,6 +45,7 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
private final FriendService friendService;
private final IMClient imClient;
private final SensitiveFilterUtil sensitiveFilterUtil;
private static final ScheduledThreadPoolExecutor EXECUTOR = ThreadPoolExecutorFactory.getThreadPoolExecutor();
@Override
public PrivateMessageVO sendMessage(PrivateMessageDTO dto) {
@ -135,8 +138,6 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
@Override
public void pullOfflineMessage(Long minId) {
UserSession session = SessionContext.getSession();
// 开启加载中标志
this.sendLoadingMessage(true);
// 获取当前用户的消息
LambdaQueryWrapper<PrivateMessage> wrapper = Wrappers.lambdaQuery();
// 只能拉取最近3个月的消息,移动端只拉取一个月消息
@ -148,21 +149,25 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
.eq(PrivateMessage::getRecvId, session.getUserId()));
wrapper.orderByAsc(PrivateMessage::getId);
List<PrivateMessage> messages = this.list(wrapper);
// 推送消息
for (PrivateMessage m : messages) {
PrivateMessageVO vo = BeanUtils.copyProperties(m, PrivateMessageVO.class);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(m.getSendId(), IMTerminalType.WEB.code()));
sendMessage.setRecvId(session.getUserId());
sendMessage.setRecvTerminals(List.of(session.getTerminal()));
sendMessage.setSendToSelf(false);
sendMessage.setData(vo);
sendMessage.setSendResult(true);
imClient.sendPrivateMessage(sendMessage);
}
// 关闭加载中标志
this.sendLoadingMessage(false);
log.info("拉取私聊消息,用户id:{},数量:{}", session.getUserId(), messages.size());
// 异步推送消息
EXECUTOR.execute(() -> {
// 开启加载中标志
this.sendLoadingMessage(true, session);
for (PrivateMessage m : messages) {
PrivateMessageVO vo = BeanUtils.copyProperties(m, PrivateMessageVO.class);
IMPrivateMessage<PrivateMessageVO> sendMessage = new IMPrivateMessage<>();
sendMessage.setSender(new IMUserInfo(m.getSendId(), IMTerminalType.WEB.code()));
sendMessage.setRecvId(session.getUserId());
sendMessage.setRecvTerminals(List.of(session.getTerminal()));
sendMessage.setSendToSelf(false);
sendMessage.setData(vo);
sendMessage.setSendResult(true);
imClient.sendPrivateMessage(sendMessage);
}
// 关闭加载中标志
this.sendLoadingMessage(false, session);
log.info("拉取私聊消息,用户id:{},数量:{}", session.getUserId(), messages.size());
});
}
@Transactional(rollbackFor = Exception.class)
@ -215,8 +220,7 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
return message.getId();
}
private void sendLoadingMessage(Boolean isLoadding) {
UserSession session = SessionContext.getSession();
private void sendLoadingMessage(Boolean isLoadding, UserSession session) {
PrivateMessageVO msgInfo = new PrivateMessageVO();
msgInfo.setType(MessageType.LOADING.code());
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;
@RedisLock(prefixKey = RedisKey.IM_LOCK_FILE_TASK)
@Scheduled(cron = "0 * * * * ?")
@Scheduled(cron = "0 0 3 * * ?")
public void run() {
log.info("【定时任务】过期文件处理...");
int batchSize = 100;

3
im-uniapp/.env.js

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

79
im-uniapp/store/chatStore.js

@ -1,7 +1,6 @@
import { defineStore } from 'pinia';
import { MESSAGE_TYPE, MESSAGE_STATUS } from '@/common/enums.js';
import useUserStore from './userStore';
import UNI_APP from '../.env';
let cacheChats = [];
export default defineStore('chatStore', {
@ -20,10 +19,6 @@ export default defineStore('chatStore', {
this.chats = [];
for (let chat of chatsData.chats) {
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)));
// 加载期间显示只前15个会话做做样子,一切都为了加快初始化时间
@ -64,6 +59,7 @@ export default defineStore('chatStore', {
lastContent: "",
lastSendTime: new Date().getTime(),
unreadCount: 0,
hotMinIdx: 0,
messages: [],
atMe: false,
atAll: false,
@ -244,24 +240,28 @@ export default defineStore('chatStore', {
deleteMessage(msgInfo, chatInfo) {
// 获取对方id或群id
let chat = this.findChat(chatInfo);
let isColdMessage = false;
for (let idx in chat.messages) {
// 已经发送成功的,根据id删除
if (chat.messages[idx].id && chat.messages[idx].id == msgInfo.id) {
chat.messages.splice(idx, 1);
isColdMessage = idx < chat.hotMinIdx;
break;
}
// 正在发送中的消息可能没有id,只有临时id
if (chat.messages[idx].tmpId && chat.messages[idx].tmpId == msgInfo.tmpId) {
chat.messages.splice(idx, 1);
isColdMessage = idx < chat.hotMinIdx;
break;
}
}
chat.stored = false;
this.saveToStorage();
this.saveToStorage(isColdMessage);
},
recallMessage(msgInfo, chatInfo) {
let chat = this.findChat(chatInfo);
if (!chat) return;
let isColdMessage = false;
// 要撤回的消息id
let id = msgInfo.content;
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) {
chat.unreadCount++;
}
isColdMessage = idx < chat.hotMinIdx;
}
// 被引用的消息也要撤回
if (m.quoteMessage && m.quoteMessage.id == msgInfo.id) {
@ -288,7 +289,7 @@ export default defineStore('chatStore', {
}
}
chat.stored = false;
this.saveToStorage();
this.saveToStorage(isColdMessage);
},
updateChatFromFriend(friend) {
let chat = this.findChatByFriend(friend.id)
@ -336,20 +337,31 @@ export default defineStore('chatStore', {
}
},
refreshChats() {
if (!cacheChats) {
return;
}
if (!cacheChats) return;
// 排序
cacheChats.sort((chat1, chat2) => {
return chat2.lastSendTime - chat1.lastSendTime;
});
cacheChats.sort((chat1, chat2) => 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;
// 清空缓存,不再使用
// 清空缓存,不再使用
cacheChats = null;
this.saveToStorage();
// 消息持久化
this.saveToStorage(true);
},
saveToStorage(state) {
saveToStorage(withColdMessage) {
// 加载中不保存,防止卡顿
if (this.isLoading()) {
return;
@ -362,12 +374,27 @@ export default defineStore('chatStore', {
this.chats.forEach((chat) => {
let chatKey = `${key}-${chat.type}-${chat.targetId}`
if (!chat.stored) {
chat.stored = true;
if (chat.delete) {
uni.removeStorageSync(chatKey);
} 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) {
chatKeys.push(chatKey);
@ -391,20 +418,26 @@ export default defineStore('chatStore', {
this.loadingPrivateMsg = false;
this.loadingGroupMsg = false;
},
loadChat(context) {
loadChat() {
return new Promise((resolve, reject) => {
let userStore = useUserStore();
let userId = userStore.userInfo.id;
let chatsData = uni.getStorageSync("chats-app-" + userId)
if (chatsData) {
if (chatsData.chatKeys) {
let time = new Date().getTime();
chatsData.chats = [];
chatsData.chatKeys.forEach(key => {
let chat = uni.getStorageSync(key);
if (chat) {
chatsData.chats.push(chat);
let coldChat = uni.getStorageSync(key);
let hotChat = uni.getStorageSync(key + '-hot');
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);

3
im-web/package.json

@ -19,8 +19,7 @@
"vue": "2.7.16",
"vue-axios": "3.5.2",
"vue-router": "3.6.5",
"vuex": "3.6.2",
"vuex-persist": "3.1.3"
"pinia": "^2.1.7"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.12",

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

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

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

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

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

@ -115,7 +115,7 @@ export default {
type: 'GROUP',
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
}).then((group) => {
this.editing = !this.editing
this.$store.commit("updateGroup", group);
this.groupStore.updateGroup(group);
this.$emit('reload');
this.$message.success("修改成功");
})
@ -139,8 +139,8 @@ export default {
url: `/group/quit/${this.group.id}`,
method: 'delete'
}).then(() => {
this.$store.commit("removeGroup", this.group.id);
this.$store.commit("removeGroupChat", this.group.id);
this.groupStore.removeGroup(this.group.id);
this.chatStore.removeGroupChat(this.group.id);
});
})
},
@ -156,7 +156,7 @@ export default {
},
computed: {
mine() {
return this.$store.state.userStore.userInfo;
return this.userStore.userInfo;
},
ownerName() {
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: {
mine() {
return this.$store.state.userStore.userInfo;
return this.userStore.userInfo;
},
histroyAction() {
return `/message/${this.chat.type.toLowerCase()}/history`;

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

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

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

@ -1,10 +1,10 @@
<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="image-box">
<img :src="url" />
</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>
</template>
@ -13,20 +13,17 @@ export default {
name: "fullImage",
data() {
return {
fit: 'contain'
show: false,
url: ''
}
},
methods: {
onClose() {
this.$emit("close");
}
},
props: {
visible: {
type: Boolean
open(url) {
this.show = true;
this.url = url;
},
url: {
type: String
close() {
this.show = false;
}
}
}

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

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

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

@ -1,5 +1,5 @@
<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="avatar">
<head-image :name="user.nickName" :url="user.headImageThumb" :size="60" :online="user.online"
@ -39,18 +39,26 @@ export default {
},
data() {
return {
}
},
props: {
user: {
type: Object
},
pos: {
type: Object
show: false,
user: {},
pos: {
x: 0,
y: 0
}
}
},
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() {
let user = this.user;
let chat = {
@ -59,12 +67,12 @@ export default {
showName: user.nickName,
headImage: user.headImage,
};
this.$store.commit("openChat", chat);
this.$store.commit("activeChat", 0);
this.chatStore.openChat(chat);
this.chatStore.setActiveChat(0);
if (this.$route.path != "/home/chat") {
this.$router.push("/home/chat");
}
this.$emit("close");
this.show = false;
},
onAddFriend() {
this.$http({
@ -82,18 +90,18 @@ export default {
online: this.user.online,
deleted: false
}
this.$store.commit("addFriend", friend);
this.friendStore.addFriend(friend);
})
},
showFullImage() {
if (this.user.headImage) {
this.$store.commit("showFullImageBox", this.user.headImage);
this.$eventBus.$emit("openFullImage", this.user.headImage);
}
}
},
computed: {
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>
</el-input>
<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="avatar">
<head-image :name="user.nickName" :url="user.headImage" :online="user.online"></head-image>
@ -15,7 +15,7 @@
<div class="nick-name">
<div>{{ user.nickName }}</div>
<div :class="user.online ? 'online-status online' : 'online-status'">{{
user.online ? "[在线]" :"[离线]"}}</div>
user.online ? "[在线]" : "[离线]" }}</div>
</div>
<div class="user-name">
<div>用户名:{{ user.userName }}</div>
@ -83,11 +83,11 @@ export default {
online: user.online,
deleted: false
}
this.$store.commit("addFriend", friend);
this.friendStore.addFriend(friend);
})
},
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() {
this.show = true;
this.friends = [];
this.$store.state.friendStore.friends.forEach((f) => {
this.friendStore.friends.forEach((f) => {
if (f.deleted) {
return;
}

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

@ -52,7 +52,7 @@ export default {
onOk() {
this.isShow = false;
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)) {
//
userInfos.push({

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

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

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

@ -56,7 +56,6 @@ export default {
}
},
methods: {
onClose() {
this.$emit("close");
},
@ -70,7 +69,7 @@ export default {
method: "put",
data: this.userInfo
}).then(() => {
this.$store.commit("setUserInfo", this.userInfo);
this.userStore.setUserInfo(this.userInfo);
this.$emit("close");
this.$message.success("修改成功");
})
@ -92,9 +91,9 @@ export default {
}
},
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));
}
}

19
im-web/src/main.js

@ -4,18 +4,25 @@ import router from './router'
import ElementUI from 'element-ui';
import './assets/style/im.scss';
import './assets/iconfont/iconfont.css';
import { createPinia, PiniaVuePlugin } from 'pinia'
import httpRequest from './api/httpRequest';
import * as socketApi from './api/wssocket';
import * as messageType from './api/messageType';
import emotion from './api/emotion.js';
import url from './api/url.js';
import element from './api/element.js';
import store from './store';
import * as enums from './api/enums.js';
import * as date from './api/date.js';
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.prototype.$wsApi = socketApi;
@ -33,7 +40,13 @@ new Vue({
el: '#app',
// 配置路由
router,
store,
pinia,
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 userStore from './userStore';
import useUserStore from './userStore.js';
import localForage from 'localforage';
/* 线cacheChats,
待所有离线消息拉取完成后再统一放至vuex中进行渲染*/
/**
* 优化1(冷热消息分区):
* 热消息登录后的消息
* 冷消息: 登录前的消息
* 每个会话的冷热消息分别用一个key进行存储当有新的消息时只更新热消息key冷消息key保持不变
* 由于热消息数量不会很大所以localForage.setItem耗时很低可以防止消息过多时出现卡顿的情况
*
* 优化2(延迟渲染):
* 拉取消息时如果直接用state.chats接收页面就开始渲染一边渲染页面一边大量接消息会导致很严重的卡顿
* 为了加速拉取离线消息效率拉取时消息暂时存储到cacheChats,等待所有离线消息拉取完成后再统一放至state中进行渲染
*
* 优化3(pinia代替vuex)
* 实测pinia的远超vuex,且语法更简洁清晰
*
* */
let cacheChats = [];
export default {
state: {
activeChat: null,
privateMsgMaxId: 0,
groupMsgMaxId: 0,
loadingPrivateMsg: false,
loadingGroupMsg: false,
chats: []
export default defineStore('chatStore', {
state: () => {
return {
activeChat: null,
privateMsgMaxId: 0,
groupMsgMaxId: 0,
loadingPrivateMsg: false,
loadingGroupMsg: false,
chats: []
}
},
mutations: {
initChats(state, chatsData) {
state.chats = [];
state.privateMsgMaxId = chatsData.privateMsgMaxId || 0;
state.groupMsgMaxId = chatsData.groupMsgMaxId || 0;
actions: {
initChats(chatsData) {
this.chats = [];
this.privateMsgMaxId = chatsData.privateMsgMaxId || 0;
this.groupMsgMaxId = chatsData.groupMsgMaxId || 0;
cacheChats = chatsData.chats || [];
// 防止图片一直处在加载中状态
cacheChats.forEach((chat) => {
@ -31,15 +47,15 @@ export default {
})
})
},
openChat(state, chatInfo) {
let chats = this.getters.findChats()
openChat(chatInfo) {
let chats = this.findChats()
let chat = null;
for (let idx in chats) {
if (chats[idx].type == chatInfo.type &&
chats[idx].targetId === chatInfo.targetId) {
chat = chats[idx];
// 放置头部
this.commit("moveTop", idx)
this.moveTop(idx)
break;
}
}
@ -53,6 +69,7 @@ export default {
lastContent: "",
lastSendTime: new Date().getTime(),
unreadCount: 0,
hotMinIdx: 0,
messages: [],
atMe: false,
atAll: false,
@ -62,12 +79,12 @@ export default {
chats.unshift(chat);
}
},
activeChat(state, idx) {
let chats = this.getters.findChats();
state.activeChat = chats[idx];
setActiveChat(idx) {
let chats = this.findChats();
this.activeChat = chats[idx];
},
resetUnreadCount(state, chatInfo) {
let chats = this.getters.findChats();
resetUnreadCount(chatInfo) {
let chats = this.findChats();
for (let idx in chats) {
if (chats[idx].type == chatInfo.type &&
chats[idx].targetId == chatInfo.targetId) {
@ -75,13 +92,13 @@ export default {
chats[idx].atMe = false;
chats[idx].atAll = false;
chats[idx].stored = false;
this.commit("saveToStorage");
this.saveToStorage();
break;
}
}
},
readedMessage(state, pos) {
let chat = this.getters.findChatByFriend(pos.friendId);
readedMessage(pos) {
let chat = this.findChatByFriend(pos.friendId);
if (!chat) return;
chat.messages.forEach((m) => {
if (m.id && m.selfSend && m.status < MESSAGE_STATUS.RECALL) {
@ -92,68 +109,69 @@ export default {
}
}
})
this.commit("saveToStorage");
this.saveToStorage();
},
removeChat(state, idx) {
let chats = this.getters.findChats();
if (chats[idx] == state.activeChat) {
state.activeChat = null;
removeChat(idx) {
let chats = this.findChats();
if (chats[idx] == this.activeChat) {
this.activeChat = null;
}
chats[idx].delete = true;
chats[idx].stored = false;
this.commit("saveToStorage");
this.saveToStorage();
},
removePrivateChat(state, friendId) {
let chats = this.getters.findChats();
removePrivateChat(friendId) {
let chats = this.findChats();
for (let idx in chats) {
if (chats[idx].type == 'PRIVATE' &&
chats[idx].targetId === friendId) {
this.commit("removeChat", idx)
this.removeChat(idx);
break;
}
}
},
removeGroupChat(state, groupId) {
let chats = this.getters.findChats();
removeGroupChat(groupId) {
let chats = this.findChats();
for (let idx in chats) {
if (chats[idx].type == 'GROUP' &&
chats[idx].targetId === groupId) {
this.commit("removeChat", idx)
this.removeChat(idx);
break;
}
}
},
moveTop(state, idx) {
moveTop(idx) {
// 加载中不移动,很耗性能
if (this.getters.isLoading()) {
if (this.isLoading()) {
return;
}
if (idx > 0) {
let chats = this.getters.findChats();
let chats = this.findChats();
let chat = chats[idx];
chats.splice(idx, 1);
chats.unshift(chat);
chat.lastSendTime = new Date().getTime();
chat.stored = false;
this.commit("saveToStorage");
this.saveToStorage();
}
},
insertMessage(state, [msgInfo, chatInfo]) {
insertMessage(msgInfo, chatInfo) {
let time = new Date().getTime()
let type = chatInfo.type;
// 记录消息的最大id
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > state.privateMsgMaxId) {
state.privateMsgMaxId = msgInfo.id;
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > this.privateMsgMaxId) {
this.privateMsgMaxId = msgInfo.id;
}
if (msgInfo.id && type == "GROUP" && msgInfo.id > state.groupMsgMaxId) {
state.groupMsgMaxId = msgInfo.id;
if (msgInfo.id && type == "GROUP" && msgInfo.id > this.groupMsgMaxId) {
this.groupMsgMaxId = msgInfo.id;
}
// 如果是已存在消息,则覆盖旧的消息数据
let chat = this.getters.findChat(chatInfo);
let message = this.getters.findMessage(chat, msgInfo);
let chat = this.findChat(chatInfo);
let message = this.findMessage(chat, msgInfo);
if (message) {
Object.assign(message, msgInfo);
chat.stored = false;
this.commit("saveToStorage");
this.saveToStorage();
return;
}
// 插入新的数据
@ -182,7 +200,8 @@ export default {
// 是否有人@我
if (!msgInfo.selfSend && chat.type == "GROUP" && msgInfo.atUserIds &&
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) {
chat.atMe = true;
}
@ -212,39 +231,44 @@ export default {
}
chat.messages.splice(insertPos, 0, msgInfo);
chat.stored = false;
this.commit("saveToStorage");
this.saveToStorage();
console.log("耗时:", new Date().getTime() - time)
},
updateMessage(state, [msgInfo, chatInfo]) {
updateMessage(msgInfo, chatInfo) {
// 获取对方id或群id
let chat = this.getters.findChat(chatInfo);
let message = this.getters.findMessage(chat, msgInfo);
let chat = this.findChat(chatInfo);
let message = this.findMessage(chat, msgInfo);
if (message) {
// 属性拷贝
Object.assign(message, msgInfo);
chat.stored = false;
this.commit("saveToStorage");
this.saveToStorage();
}
},
deleteMessage(state, [msgInfo, chatInfo]) {
let chat = this.getters.findChat(chatInfo);
deleteMessage(msgInfo, chatInfo) {
let chat = this.findChat(chatInfo);
let isColdMessage = false;
for (let idx in chat.messages) {
// 已经发送成功的,根据id删除
if (chat.messages[idx].id && chat.messages[idx].id == msgInfo.id) {
chat.messages.splice(idx, 1);
isColdMessage = idx < chat.hotMinIdx;
break;
}
// 正在发送中的消息可能没有id,只有临时id
if (chat.messages[idx].tmpId && chat.messages[idx].tmpId == msgInfo.tmpId) {
chat.messages.splice(idx, 1);
isColdMessage = idx < chat.hotMinIdx;
break;
}
}
chat.stored = false;
this.commit("saveToStorage");
this.saveToStorage(isColdMessage);
},
recallMessage(state, [msgInfo, chatInfo]) {
let chat = this.getters.findChat(chatInfo);
recallMessage(msgInfo, chatInfo) {
let chat = this.findChat(chatInfo);
if (!chat) return;
let isColdMessage = false;
// 要撤回的消息id
let id = msgInfo.content;
let name = msgInfo.selfSend ? '你' : chat.type == 'PRIVATE' ? '对方' : msgInfo.sendNickName;
@ -262,6 +286,7 @@ export default {
if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED) {
chat.unreadCount++;
}
isColdMessage = idx < chat.hotMinIdx;
}
// 被引用的消息也要撤回
if (m.quoteMessage && m.quoteMessage.id == msgInfo.id) {
@ -271,86 +296,100 @@ export default {
}
}
chat.stored = false;
this.commit("saveToStorage");
this.saveToStorage(isColdMessage);
},
updateChatFromFriend(state, friend) {
let chat = this.getters.findChatByFriend(friend.id);
updateChatFromFriend(friend) {
let chat = this.findChatByFriend(friend.id);
// 更新会话中的群名和头像
if (chat && (chat.headImage != friend.headImage ||
chat.showName != friend.nickName)) {
chat.showName != friend.nickName)) {
chat.headImage = friend.headImage;
chat.showName = friend.nickName;
chat.stored = false;
this.commit("saveToStorage")
this.saveToStorage()
}
},
updateChatFromUser(user) {
let chat = this.getters.findChatByFriend(user.id);
let chat = this.findChatByFriend(user.id);
// 更新会话中的昵称和头像
if (chat && (chat.headImage != user.headImageThumb ||
chat.showName != user.nickName)) {
chat.showName != user.nickName)) {
chat.headImage = user.headImageThumb;
chat.showName = user.nickName;
chat.stored = false;
this.saveToStorage();
}
},
updateChatFromGroup(state, group) {
let chat = this.getters.findChatByGroup(group.id);
updateChatFromGroup(group) {
let chat = this.findChatByGroup(group.id);
if (chat && (chat.headImage != group.headImageThumb ||
chat.showName != group.showGroupName)) {
chat.showName != group.showGroupName)) {
// 更新会话中的群名称和头像
chat.headImage = group.headImageThumb;
chat.showName = group.showGroupName;
chat.stored = false;
this.commit("saveToStorage")
this.saveToStorage()
}
},
loadingPrivateMsg(state, loading) {
state.loadingPrivateMsg = loading;
if (!this.getters.isLoading()) {
this.commit("refreshChats")
setLoadingPrivateMsg(loading) {
this.loadingPrivateMsg = loading;
if (!this.isLoading()) {
this.refreshChats();
}
},
loadingGroupMsg(state, loading) {
state.loadingGroupMsg = loading;
if (!this.getters.isLoading()) {
this.commit("refreshChats")
setLoadingGroupMsg(loading) {
this.loadingGroupMsg = loading;
if (!this.isLoading()) {
this.refreshChats();
}
},
refreshChats(state) {
if (!cacheChats) {
return;
}
refreshChats() {
if (!cacheChats) return;
// 排序
cacheChats.sort((chat1, chat2) => {
return chat2.lastSendTime - chat1.lastSendTime;
});
cacheChats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime);
// 记录热数据索引位置
cacheChats.forEach(chat => chat.hotMinIdx = chat.messages.length);
// 将消息一次性装载回来
state.chats = cacheChats;
this.chats = cacheChats;
// 清空缓存
cacheChats = null;
this.commit("saveToStorage");
// 持久化消息
this.saveToStorage(true);
},
saveToStorage(state) {
saveToStorage(withColdMessage) {
// 加载中不保存,防止卡顿
if (this.getters.isLoading()) {
if (this.isLoading()) {
return;
}
let userId = userStore.state.userInfo.id;
let userStore = useUserStore();
let userId = userStore.userInfo.id;
let key = "chats-" + userId;
let chatKeys = [];
// 按会话为单位存储,
state.chats.forEach((chat) => {
this.chats.forEach((chat) => {
// 只存储有改动的会话
let chatKey = `${key}-${chat.type}-${chat.targetId}`
if (!chat.stored) {
chat.stored = true;
if (chat.delete) {
localForage.removeItem(chatKey);
} 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) {
chatKeys.push(chatKey);
@ -358,46 +397,56 @@ export default {
})
// 会话核心信息
let chatsData = {
privateMsgMaxId: state.privateMsgMaxId,
groupMsgMaxId: state.groupMsgMaxId,
privateMsgMaxId: this.privateMsgMaxId,
groupMsgMaxId: this.groupMsgMaxId,
chatKeys: chatKeys
}
localForage.setItem(key, chatsData)
// 清理已删除的会话
state.chats = state.chats.filter(chat => !chat.delete)
this.chats = this.chats.filter(chat => !chat.delete)
},
clear(state) {
clear() {
cacheChats = []
state.chats = [];
state.activeChat = null;
}
},
actions: {
loadChat(context) {
this.chats = [];
this.activeChat = null;
},
loadChat() {
return new Promise((resolve, reject) => {
let userId = userStore.state.userInfo.id;
let userStore = useUserStore();
let userId = userStore.userInfo.id;
let key = "chats-" + userId;
localForage.getItem(key).then((chatsData) => {
if (!chatsData) {
resolve();
} else if (chatsData.chats) {
// 兼容旧版本
context.commit("initChats", chatsData);
resolve();
} else if (chatsData.chatKeys) {
const promises = [];
chatsData.chatKeys.forEach(key => {
promises.push(localForage.getItem(key))
promises.push(localForage.getItem(key + "-hot"))
})
Promise.all(promises).then(chats => {
chatsData.chats = chats.filter(o => o);
context.commit("initChats", chatsData);
chatsData.chats = [];
// 偶数下标为冷消息,奇数下标为热消息
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();
})
}
}).catch((e) => {
}).catch(e => {
console.log("加载消息失败")
reject();
reject(e);
})
})
}
@ -406,14 +455,14 @@ export default {
isLoading: (state) => () => {
return state.loadingPrivateMsg || state.loadingGroupMsg
},
findChats: (state, getters) => () => {
if (cacheChats && getters.isLoading()) {
findChats: (state) => () => {
if (cacheChats && state.isLoading()) {
return cacheChats;
}
return state.chats;
},
findChatIdx: (state, getters) => (chat) => {
let chats = getters.findChats();
findChatIdx: (state) => (chat) => {
let chats = state.findChats();
for (let idx in chats) {
if (chats[idx].type == chat.type &&
chats[idx].targetId === chat.targetId) {
@ -422,18 +471,18 @@ export default {
}
}
},
findChat: (state, getters) => (chat) => {
let chats = getters.findChats();
let idx = getters.findChatIdx(chat);
findChat: (state) => (chat) => {
let chats = state.findChats();
let idx = state.findChatIdx(chat);
return chats[idx];
},
findChatByFriend: (state, getters) => (fid) => {
let chats = getters.findChats();
findChatByFriend: (state) => (fid) => {
let chats = state.findChats();
return chats.find(chat => chat.type == 'PRIVATE' &&
chat.targetId == fid)
},
findChatByGroup: (state, getters) => (gid) => {
let chats = getters.findChats();
findChatByGroup: (state) => (gid) => {
let chats = state.findChats();
return chats.find(chat => chat.type == 'GROUP' &&
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'
export default {
state: {
webrtc: {}
},
mutations: {
setConfig(state, config) {
state.webrtc = config.webrtc;
},
clear(state) {
state.webrtc = {};
export default defineStore('configStore', {
state: () => {
return {
webrtc: {}
}
},
actions: {
loadConfig(context) {
setConfig(config) {
this.webrtc = config.webrtc;
},
loadConfig() {
return new Promise((resolve, reject) => {
http({
url: '/system/config',
method: 'GET'
}).then((config) => {
}).then(config => {
console.log("系统配置", config)
context.commit("setConfig", config);
this.setConfig(config);
resolve();
}).catch((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 { TERMINAL_TYPE } from "../api/enums.js"
export default {
state: {
friends: [],
timer: null
export default defineStore('friendStore', {
state: () => {
return {
friends: [], // 好友列表
timer: null
}
},
mutations: {
setFriends(state, friends) {
friends.forEach((f) => {
f.online = false;
f.onlineWeb = false;
f.onlineApp = false;
})
state.friends = friends;
actions: {
setFriends(friends) {
this.friends = friends;
},
updateFriend(state, friend) {
state.friends.forEach((f, index) => {
updateFriend(friend) {
this.friends.forEach((f, index) => {
if (f.id == friend.id) {
// 拷贝属性
let online = state.friends[index].online;
Object.assign(state.friends[index], friend);
state.friends[index].online = online;
let online = this.friends[index].online;
Object.assign(this.friends[index], friend);
this.friends[index].online = online;
}
})
},
removeFriend(state, id) {
state.friends.filter(f => f.id == id).forEach(f => f.deleted = true);
removeFriend(id) {
this.friends.filter(f => f.id == id).forEach(f => f.deleted = true);
},
addFriend(state, friend) {
if (state.friends.some((f) => f.id == friend.id)) {
this.commit("updateFriend", friend)
addFriend(friend) {
if (this.friends.some(f => f.id == friend.id)) {
this.updateFriend(friend)
} else {
state.friends.unshift(friend);
this.friends.unshift(friend);
}
},
refreshOnlineStatus(state) {
let userIds = state.friends.filter((f) => !f.deleted).map((f) => f.id);
if (userIds.length == 0) {
return;
updateOnlineStatus(onlineData) {
let friend = this.findFriend(onlineData.userId);
if (onlineData.terminal == TERMINAL_TYPE.WEB) {
friend.onlineWeb = onlineData.online;
} else if (onlineData.terminal == TERMINAL_TYPE.APP) {
friend.onlineApp = onlineData.online;
}
http({
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)
friend.online = friend.onlineWeb || friend.onlineApp;
},
setOnlineStatus(state, onlineTerminals) {
state.friends.forEach((f) => {
let userTerminal = onlineTerminals.find((o) => f.id == o.userId);
if (userTerminal) {
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() {
this.timer && clearTimeout(this.timer);
this.friends = [];
this.timer = null;
},
clear(state) {
state.timer && clearTimeout(state.timer);
state.friends = [];
state.timer = null;
}
},
actions: {
loadFriend(context) {
loadFriend() {
return new Promise((resolve, reject) => {
http({
url: '/friend/list',
method: 'GET'
}).then((friends) => {
context.commit("setFriends", friends);
context.commit("refreshOnlineStatus");
resolve()
}).catch(() => {
reject();
}).then(async (friends) => {
this.setFriends(friends);
resolve();
}).catch(e => {
reject(e);
})
});
}
@ -108,4 +69,4 @@ export default {
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'
export default {
state: {
groups: []
export default defineStore('groupStore', {
state: () => {
return {
groups: []
}
},
mutations: {
setGroups(state, groups) {
state.groups = groups;
actions: {
setGroups(groups) {
this.groups = groups;
},
addGroup(state, group) {
if (state.groups.some((g) => g.id == group.id)) {
this.commit("updateGroup", group)
addGroup(group) {
if (this.groups.some(g => g.id == group.id)) {
this.updateGroup(group)
} else {
state.groups.unshift(group);
this.groups.unshift(group);
}
},
removeGroup(state, id) {
state.groups.filter(g => g.id == id).forEach(g => g.quit = true);
removeGroup(id) {
this.groups.filter(g => g.id == id).forEach(g => g.quit = true);
},
updateGroup(state, group) {
state.groups.forEach((g, idx) => {
updateGroup(group) {
this.groups.forEach((g, idx) => {
if (g.id == group.id) {
// 拷贝属性
Object.assign(state.groups[idx], group);
Object.assign(this.groups[idx], group);
}
})
},
clear(state) {
state.groups = [];
}
},
actions: {
loadGroup(context) {
updateTopMessage(id, topMessage) {
let group = this.findGroup(id);
if (group) {
group.topMessage = topMessage;
}
},
clear() {
this.groups = [];
},
loadGroup() {
return new Promise((resolve, reject) => {
http({
url: '/group/list',
method: 'GET'
}).then((groups) => {
context.commit("setGroups", groups);
}).then(groups => {
this.setGroups(groups);
resolve();
}).catch((res) => {
reject(res);
}).catch(e => {
reject(e);
})
});
}
},
getters: {
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 { RTC_STATE } from "../api/enums.js"
export default {
state: {
userInfo: {
},
rtcInfo: {
friend: {}, // 好友信息
mode: "video", // 模式 video:视频 voice:语音
state: RTC_STATE.FREE // FREE:空闲 WAIT_CALL:呼叫方等待 WAIT_ACCEPT: 被呼叫方等待接听 CHATING:聊天中
export default defineStore('userStore', {
state: () => {
return {
userInfo: {},
rtcInfo: {
friend: {}, // 好友信息
mode: "video", // 模式 video:视频 voice:语音
state: RTC_STATE.FREE // FREE:空闲 WAIT_CALL:呼叫方等待 WAIT_ACCEPT: 被呼叫方等待接听 CHATING:聊天中
}
}
},
mutations: {
setUserInfo(state, userInfo) {
state.userInfo = userInfo
actions: {
setUserInfo(userInfo) {
this.userInfo = userInfo
},
setRtcInfo(state, rtcInfo) {
state.rtcInfo = rtcInfo;
setRtcInfo(rtcInfo) {
this.rtcInfo = rtcInfo;
},
setRtcState(state, rtcState) {
state.rtcInfo.state = rtcState;
setRtcState(rtcState) {
this.rtcInfo.state = rtcState;
},
clear(state) {
state.userInfo = {};
state.rtcInfo = {
clear() {
this.userInfo = {};
this.rtcInfo = {
friend: {},
mode: "video",
state: RTC_STATE.FREE
};
}
},
actions: {
loadUser(context) {
},
loadUser() {
return new Promise((resolve, reject) => {
http({
url: '/user/self',
method: 'GET'
}).then((userInfo) => {
context.commit("setUserInfo", userInfo);
this.setUserInfo(userInfo);
resolve();
}).catch((res) => {
reject(res);
@ -47,4 +45,4 @@ export default {
})
}
}
}
});

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

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

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

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

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

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

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

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

Loading…
Cancel
Save