Browse Source

!159 优化离线消息拉取效率

Merge pull request !159 from blue/v_3.0.0
master
blue 8 months ago
committed by Gitee
parent
commit
ccaeafeebb
No known key found for this signature in database GPG Key ID: 173E9B9CA92EEF8F
  1. 4
      im-platform/src/main/java/com/bx/implatform/controller/FileController.java
  2. 9
      im-platform/src/main/java/com/bx/implatform/controller/GroupMessageController.java
  3. 9
      im-platform/src/main/java/com/bx/implatform/controller/PrivateMessageController.java
  4. 2
      im-platform/src/main/java/com/bx/implatform/service/FileService.java
  5. 8
      im-platform/src/main/java/com/bx/implatform/service/GroupMessageService.java
  6. 8
      im-platform/src/main/java/com/bx/implatform/service/PrivateMessageService.java
  7. 6
      im-platform/src/main/java/com/bx/implatform/service/impl/FileServiceImpl.java
  8. 2
      im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java
  9. 83
      im-platform/src/main/java/com/bx/implatform/service/impl/GroupMessageServiceImpl.java
  10. 36
      im-platform/src/main/java/com/bx/implatform/service/impl/PrivateMessageServiceImpl.java
  11. 3
      im-platform/src/main/java/com/bx/implatform/service/impl/UserServiceImpl.java
  12. 93
      im-uniapp/App.vue
  13. 1
      im-uniapp/common/request.js
  14. 2
      im-uniapp/components/chat-item/chat-item.vue
  15. 2
      im-uniapp/components/group-item/group-item.vue
  16. 7
      im-uniapp/components/image-upload/image-upload.vue
  17. 1
      im-uniapp/main.js
  18. 1
      im-uniapp/package.json
  19. 42
      im-uniapp/pages/chat/chat-box.vue
  20. 2
      im-uniapp/pages/chat/chat.vue
  21. 2
      im-uniapp/pages/common/user-info.vue
  22. 2
      im-uniapp/pages/friend/friend-add.vue
  23. 11
      im-uniapp/pages/friend/friend.vue
  24. 36
      im-uniapp/pages/group/group-edit.vue
  25. 2
      im-uniapp/pages/group/group-info.vue
  26. 2
      im-uniapp/pages/mine/mine-edit.vue
  27. 86
      im-uniapp/store/chatStore.js
  28. 11
      im-web/src/components/chat/ChatBox.vue
  29. 2
      im-web/src/components/chat/ChatMessageItem.vue
  30. 5
      im-web/src/components/common/FileUpload.vue
  31. 2
      im-web/src/components/common/UserInfo.vue
  32. 2
      im-web/src/components/friend/AddFriend.vue
  33. 2
      im-web/src/components/group/GroupItem.vue
  34. 2
      im-web/src/components/setting/Setting.vue
  35. 80
      im-web/src/store/chatStore.js
  36. 37
      im-web/src/store/friendStore.js
  37. 2
      im-web/src/view/Chat.vue
  38. 2
      im-web/src/view/Friend.vue
  39. 4
      im-web/src/view/Group.vue
  40. 82
      im-web/src/view/Home.vue

4
im-platform/src/main/java/com/bx/implatform/controller/FileController.java

@ -25,8 +25,8 @@ public class FileController {
@Operation(summary = "上传图片", description = "上传图片,上传后返回原图和缩略图的url") @Operation(summary = "上传图片", description = "上传图片,上传后返回原图和缩略图的url")
@PostMapping("/image/upload") @PostMapping("/image/upload")
public Result<UploadImageVO> uploadImage(@RequestParam("file") MultipartFile file, public Result<UploadImageVO> uploadImage(@RequestParam("file") MultipartFile file,
@RequestParam(defaultValue = "true") Boolean isPermanent) { @RequestParam(defaultValue = "true") Boolean isPermanent, @RequestParam(defaultValue = "50") Long thumbSize) {
return ResultUtils.success(fileService.uploadImage(file,isPermanent)); return ResultUtils.success(fileService.uploadImage(file, isPermanent,thumbSize));
} }
@Operation(summary = "上传文件", description = "上传文件,上传后返回文件url") @Operation(summary = "上传文件", description = "上传文件,上传后返回文件url")

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

@ -35,12 +35,19 @@ public class GroupMessageController {
} }
@GetMapping("/pullOfflineMessage") @GetMapping("/pullOfflineMessage")
@Operation(summary = "拉取离线消息", description = "拉取离线消息,消息将通过webscoket异步推送") @Operation(summary = "拉取离线消息(已废弃)", description = "拉取离线消息,消息将通过webscoket异步推送")
public Result pullOfflineMessage(@RequestParam Long minId) { public Result pullOfflineMessage(@RequestParam Long minId) {
groupMessageService.pullOfflineMessage(minId); groupMessageService.pullOfflineMessage(minId);
return ResultUtils.success(); return ResultUtils.success();
} }
@GetMapping(value = "/loadOfflineMessage")
@Operation(summary = "拉取离线消息", description = "拉取离线消息")
public Result<List<GroupMessageVO>> loadOfflineMessage(@RequestParam Long minId) {
return ResultUtils.success(groupMessageService.loadOffineMessage(minId));
}
@PutMapping("/readed") @PutMapping("/readed")
@Operation(summary = "消息已读", description = "将群聊中的消息状态置为已读") @Operation(summary = "消息已读", description = "将群聊中的消息状态置为已读")
public Result readedMessage(@RequestParam Long groupId) { public Result readedMessage(@RequestParam Long groupId) {

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

@ -35,12 +35,19 @@ public class PrivateMessageController {
} }
@GetMapping("/pullOfflineMessage") @GetMapping("/pullOfflineMessage")
@Operation(summary = "拉取离线消息", description = "拉取离线消息,消息将通过webscoket异步推送") @Operation(summary = "拉取离线消息(已废弃)", description = "拉取离线消息,消息将通过webscoket异步推送")
public Result pullOfflineMessage(@RequestParam Long minId) { public Result pullOfflineMessage(@RequestParam Long minId) {
privateMessageService.pullOfflineMessage(minId); privateMessageService.pullOfflineMessage(minId);
return ResultUtils.success(); return ResultUtils.success();
} }
@GetMapping(value = "/loadOfflineMessage")
@Operation(summary = "拉取离线消息", description = "拉取离线消息")
public Result<List<PrivateMessageVO>> loadOfflineMessage(@RequestParam Long minId) {
return ResultUtils.success(privateMessageService.loadOfflineMessage(minId));
}
@PutMapping("/readed") @PutMapping("/readed")
@Operation(summary = "消息已读", description = "将会话中接收的消息状态置为已读") @Operation(summary = "消息已读", description = "将会话中接收的消息状态置为已读")
public Result readedMessage(@RequestParam Long friendId) { public Result readedMessage(@RequestParam Long friendId) {

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

@ -9,7 +9,7 @@ public interface FileService extends IService<FileInfo> {
String uploadFile(MultipartFile file); String uploadFile(MultipartFile file);
UploadImageVO uploadImage(MultipartFile file,Boolean isPermanent); UploadImageVO uploadImage(MultipartFile file,Boolean isPermanent,Long thumbSize);
} }

8
im-platform/src/main/java/com/bx/implatform/service/GroupMessageService.java

@ -31,6 +31,14 @@ public interface GroupMessageService extends IService<GroupMessage> {
*/ */
void pullOfflineMessage(Long minId); void pullOfflineMessage(Long minId);
/**
* 拉取离线消息只能拉取最近1个月的消息
*
* @param minId 消息起始id
*/
List<GroupMessageVO> loadOffineMessage(Long minId);
/** /**
* 消息已读,同步其他终端清空未读数量 * 消息已读,同步其他终端清空未读数量
* *

8
im-platform/src/main/java/com/bx/implatform/service/PrivateMessageService.java

@ -43,6 +43,14 @@ public interface PrivateMessageService extends IService<PrivateMessage> {
*/ */
void pullOfflineMessage(Long minId); void pullOfflineMessage(Long minId);
/**
* 拉取离线消息只能拉取最近1个月的消息
*
* @param minId 消息起始id
*/
List<PrivateMessageVO> loadOfflineMessage(Long minId);
/** /**
* 消息已读,将整个会话的消息都置为已读状态 * 消息已读,将整个会话的消息都置为已读状态
* *

6
im-platform/src/main/java/com/bx/implatform/service/impl/FileServiceImpl.java

@ -92,7 +92,7 @@ public class FileServiceImpl extends ServiceImpl<FileInfoMapper, FileInfo> imple
@Transactional @Transactional
@Override @Override
public UploadImageVO uploadImage(MultipartFile file, Boolean isPermanent) { public UploadImageVO uploadImage(MultipartFile file, Boolean isPermanent,Long thumbSize) {
try { try {
Long userId = SessionContext.getSession().getUserId(); Long userId = SessionContext.getSession().getUserId();
// 大小校验 // 大小校验
@ -129,9 +129,9 @@ public class FileServiceImpl extends ServiceImpl<FileInfoMapper, FileInfo> imple
throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败"); throw new GlobalException(ResultCode.PROGRAM_ERROR, "图片上传失败");
} }
vo.setOriginUrl(generUrl(FileType.IMAGE, fileName)); vo.setOriginUrl(generUrl(FileType.IMAGE, fileName));
if (file.getSize() > 50 * 1024) { if (file.getSize() > thumbSize * 1024) {
// 大于50K的文件需上传缩略图 // 大于50K的文件需上传缩略图
byte[] imageByte = ImageUtil.compressForScale(file.getBytes(), 30); byte[] imageByte = ImageUtil.compressForScale(file.getBytes(), thumbSize);
String thumbFileName = minioSerivce.upload(minioProps.getBucketName(), minioProps.getImagePath(), String thumbFileName = minioSerivce.upload(minioProps.getBucketName(), minioProps.getImagePath(),
file.getOriginalFilename(), imageByte, file.getContentType()); file.getOriginalFilename(), imageByte, file.getContentType());
if (StringUtils.isEmpty(thumbFileName)) { if (StringUtils.isEmpty(thumbFileName)) {

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

@ -132,7 +132,7 @@ public class FriendServiceImpl extends ServiceImpl<FriendMapper, Friend> impleme
friend.setUserId(userId); friend.setUserId(userId);
friend.setFriendId(friendId); friend.setFriendId(friendId);
User friendInfo = userMapper.selectById(friendId); User friendInfo = userMapper.selectById(friendId);
friend.setFriendHeadImage(friendInfo.getHeadImage()); friend.setFriendHeadImage(friendInfo.getHeadImageThumb());
friend.setFriendNickName(friendInfo.getNickName()); friend.setFriendNickName(friendInfo.getNickName());
friend.setDeleted(false); friend.setDeleted(false);
this.saveOrUpdate(friend); this.saveOrUpdate(friend);

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

@ -80,6 +80,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
msg.setSendTime(new Date()); msg.setSendTime(new Date());
msg.setSendNickName(member.getShowNickName()); msg.setSendNickName(member.getShowNickName());
msg.setAtUserIds(CommaTextUtils.asText(dto.getAtUserIds())); msg.setAtUserIds(CommaTextUtils.asText(dto.getAtUserIds()));
msg.setStatus(MessageStatus.PENDING.code());
// 过滤内容中的敏感词 // 过滤内容中的敏感词
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()));
@ -164,10 +165,7 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
wrapper.gt(GroupMessage::getSendTime, minDate); wrapper.gt(GroupMessage::getSendTime, minDate);
wrapper.in(GroupMessage::getGroupId, groupIds); wrapper.in(GroupMessage::getGroupId, groupIds);
wrapper.orderByDesc(GroupMessage::getId); wrapper.orderByDesc(GroupMessage::getId);
if (minId <= 0) { wrapper.last("limit 50000");
// 首次拉取限制消息数量大小,防止内存溢出
wrapper.last("limit 100000");
}
List<GroupMessage> messages = this.list(wrapper); List<GroupMessage> messages = this.list(wrapper);
// 通过群聊对消息进行分组 // 通过群聊对消息进行分组
Map<Long, List<GroupMessage>> messageGroupMap = Map<Long, List<GroupMessage>> messageGroupMap =
@ -254,6 +252,83 @@ public class GroupMessageServiceImpl extends ServiceImpl<GroupMessageMapper, Gro
}); });
} }
@Override
public List<GroupMessageVO> loadOffineMessage(Long minId) {
UserSession session = SessionContext.getSession();
// 查询用户加入的群组
List<GroupMember> members = groupMemberService.findByUserId(session.getUserId());
Map<Long, GroupMember> groupMemberMap = CollStreamUtil.toIdentityMap(members, GroupMember::getGroupId);
Set<Long> groupIds = groupMemberMap.keySet();
// 只能拉取最近1个月的消息
Date minDate = DateUtils.addMonths(new Date(), -1);
LambdaQueryWrapper<GroupMessage> wrapper = Wrappers.lambdaQuery();
wrapper.gt(GroupMessage::getId, minId);
wrapper.gt(GroupMessage::getSendTime, minDate);
wrapper.in(GroupMessage::getGroupId, groupIds);
wrapper.orderByDesc(GroupMessage::getId);
wrapper.last("limit 50000");
List<GroupMessage> messages = this.list(wrapper);
// 退群前的消息
List<GroupMember> quitMembers = groupMemberService.findQuitInMonth(session.getUserId());
for (GroupMember quitMember : quitMembers) {
wrapper = Wrappers.lambdaQuery();
wrapper.gt(GroupMessage::getId, minId);
wrapper.between(GroupMessage::getSendTime, minDate, quitMember.getQuitTime());
wrapper.eq(GroupMessage::getGroupId, quitMember.getGroupId());
wrapper.orderByDesc(GroupMessage::getId);
wrapper.last("limit 1000");
List<GroupMessage> groupMessages = this.list(wrapper);
if (!groupMessages.isEmpty()) {
messages.addAll(groupMessages);
groupMemberMap.put(quitMember.getGroupId(), quitMember);
}
}
// 通过群聊对消息进行分组
Map<Long, List<GroupMessage>> messageGroupMap =
messages.stream().collect(Collectors.groupingBy(GroupMessage::getGroupId));
List<GroupMessageVO> vos = new LinkedList<>();
for (Map.Entry<Long, List<GroupMessage>> entry : messageGroupMap.entrySet()) {
Long groupId = entry.getKey();
List<GroupMessage> groupMessages = entry.getValue();
// 填充消息状态
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;
}
// 排除不需要接收的消息
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.PENDING.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);
}
vos.add(vo);
}
}
// 排序
return vos.stream().sorted(Comparator.comparing(GroupMessageVO::getId)).collect(Collectors.toList());
}
@Override @Override
public void readedMessage(Long groupId) { public void readedMessage(Long groupId) {
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();

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

@ -32,6 +32,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.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -140,9 +141,8 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
UserSession session = SessionContext.getSession(); UserSession session = SessionContext.getSession();
// 获取当前用户的消息 // 获取当前用户的消息
LambdaQueryWrapper<PrivateMessage> wrapper = Wrappers.lambdaQuery(); LambdaQueryWrapper<PrivateMessage> wrapper = Wrappers.lambdaQuery();
// 只能拉取最近3个月的消息,移动端只拉取一个月消息 // 只能拉取最近1个月的消息
int months = session.getTerminal().equals(IMTerminalType.APP.code()) ? 1 : 3; Date minDate = DateUtils.addMonths(new Date(), -1);
Date minDate = DateUtils.addMonths(new Date(), -months);
wrapper.gt(PrivateMessage::getId, minId); wrapper.gt(PrivateMessage::getId, minId);
wrapper.ge(PrivateMessage::getSendTime, minDate); wrapper.ge(PrivateMessage::getSendTime, minDate);
wrapper.and(wp -> wp.eq(PrivateMessage::getSendId, session.getUserId()).or() wrapper.and(wp -> wp.eq(PrivateMessage::getSendId, session.getUserId()).or()
@ -175,6 +175,36 @@ public class PrivateMessageServiceImpl extends ServiceImpl<PrivateMessageMapper,
}); });
} }
@Override
public List<PrivateMessageVO> loadOfflineMessage(Long minId) {
UserSession session = SessionContext.getSession();
// 获取当前用户的消息
LambdaQueryWrapper<PrivateMessage> wrapper = Wrappers.lambdaQuery();
// 只能拉取最近1个月的消息
Date minDate = DateUtils.addMonths(new Date(), -1);
wrapper.gt(PrivateMessage::getId, minId);
wrapper.ge(PrivateMessage::getSendTime, minDate);
wrapper.and(wp -> wp.eq(PrivateMessage::getSendId, session.getUserId()).or()
.eq(PrivateMessage::getRecvId, session.getUserId()));
wrapper.orderByAsc(PrivateMessage::getId);
List<PrivateMessage> messages = this.list(wrapper);
// 更新消息为送达状态
List<Long> messageIds =
messages.stream().filter(m -> m.getStatus().equals(MessageStatus.PENDING.code())).map(PrivateMessage::getId)
.collect(Collectors.toList());
if (!messageIds.isEmpty()) {
LambdaUpdateWrapper<PrivateMessage> updateWrapper = Wrappers.lambdaUpdate();
updateWrapper.in(PrivateMessage::getId, messageIds);
updateWrapper.set(PrivateMessage::getStatus, MessageStatus.DELIVERED.code());
update(updateWrapper);
}
// 转换vo
List<PrivateMessageVO> vos = messages.stream().map(m -> BeanUtils.copyProperties(m, PrivateMessageVO.class))
.collect(Collectors.toList());
log.info("拉取私聊消息,用户id:{},数量:{},minId:{}", session.getUserId(), messages.size(), minId);
return vos;
}
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@Override @Override
public void readedMessage(Long friendId) { public void readedMessage(Long friendId) {

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

@ -151,9 +151,6 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
if(!vo.getNickName().equals(sensitiveFilterUtil.filter(vo.getNickName()))){ if(!vo.getNickName().equals(sensitiveFilterUtil.filter(vo.getNickName()))){
throw new GlobalException("昵称包含敏感字符"); throw new GlobalException("昵称包含敏感字符");
} }
if(!vo.getSignature().equals(sensitiveFilterUtil.filter(vo.getSignature()))){
throw new GlobalException("签名内容包含敏感字符");
}
if (!session.getUserId().equals(vo.getId())) { if (!session.getUserId().equals(vo.getId())) {
throw new GlobalException("不允许修改其他用户的信息"); throw new GlobalException("不允许修改其他用户的信息");
} }

93
im-uniapp/App.vue

@ -11,7 +11,9 @@ export default {
return { return {
isExit: false, // 退 isExit: false, // 退
audioTip: null, audioTip: null,
reconnecting: false // reconnecting: false, //
privateMessagesBuffer: [],
groupMessagesBuffer: [],
} }
}, },
methods: { methods: {
@ -36,8 +38,7 @@ export default {
this.onReconnectWs(); this.onReconnectWs();
} else { } else {
// 线 // 线
this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId); this.pullOfflineMessage();
this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId);
this.configStore.setAppInit(true); this.configStore.setAppInit(true);
} }
}); });
@ -50,11 +51,21 @@ export default {
}) })
this.exit(); this.exit();
} else if (cmd == 3) { } else if (cmd == 3) {
// if (!this.configStore.appInit || this.chatStore.loading) {
this.handlePrivateMessage(msgInfo); // 线
this.privateMessagesBuffer.push(msgInfo);
} else {
//
this.handlePrivateMessage(msgInfo);
}
} else if (cmd == 4) { } else if (cmd == 4) {
// if (!this.configStore.appInit || this.chatStore.loading) {
this.handleGroupMessage(msgInfo); // 线
this.privateMessagesBuffer.push(msgInfo);
} else {
//
this.handleGroupMessage(msgInfo);
}
} else if (cmd == 5) { } else if (cmd == 5) {
// //
this.handleSystemMessage(msgInfo); this.handleSystemMessage(msgInfo);
@ -84,30 +95,43 @@ export default {
this.configStore.clear(); this.configStore.clear();
this.userStore.clear(); this.userStore.clear();
}, },
pullOfflineMessage() {
this.chatStore.setLoading(true);
const promises = [];
promises.push(this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId));
promises.push(this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId));
Promise.all(promises).then(messages => {
// 线
messages[0].forEach(m => this.handlePrivateMessage(m));
messages[1].forEach(m => this.handleGroupMessage(m));
//
this.privateMessagesBuffer.forEach(m => this.handlePrivateMessage(m));
this.groupMessagesBuffer.forEach(m => this.handleGroupMessage(m));
//
this.privateMessagesBuffer = [];
this.groupMessagesBuffer = [];
// 线
this.chatStore.setLoading(false);
//
this.chatStore.refreshChats();
}).catch((e) => {
console.log(e)
this.$message.error("拉取离线消息失败");
this.onExit();
})
},
pullPrivateOfflineMessage(minId) { pullPrivateOfflineMessage(minId) {
this.chatStore.setLoadingPrivateMsg(true) return this.$http({
http({ url: "/message/private/loadOfflineMessage?minId=" + minId,
url: "/message/private/pullOfflineMessage?minId=" + minId, method: 'GET',
method: 'GET' timeout: 60000
}).catch(() => {
uni.showToast({
title: "消息拉取失败,请重新登陆",
icon: 'none'
})
this.exit()
}) })
}, },
pullGroupOfflineMessage(minId) { pullGroupOfflineMessage(minId) {
this.chatStore.setLoadingGroupMsg(true) return this.$http({
http({ url: "/message/group/loadOfflineMessage?minId=" + minId,
url: "/message/group/pullOfflineMessage?minId=" + minId, method: 'GET',
method: 'GET' timeout: 60000
}).catch(() => {
uni.showToast({
title: "消息拉取失败,请重新登陆",
icon: 'none'
})
this.exit()
}) })
}, },
handlePrivateMessage(msg) { handlePrivateMessage(msg) {
@ -120,11 +144,6 @@ export default {
type: 'PRIVATE', type: 'PRIVATE',
targetId: friendId targetId: friendId
} }
//
if (msg.type == enums.MESSAGE_TYPE.LOADING) {
this.chatStore.setLoadingPrivateMsg(JSON.parse(msg.content))
return;
}
// //
if (msg.type == enums.MESSAGE_TYPE.READED) { if (msg.type == enums.MESSAGE_TYPE.READED) {
this.chatStore.resetUnreadCount(chatInfo); this.chatStore.resetUnreadCount(chatInfo);
@ -203,8 +222,7 @@ export default {
// //
this.chatStore.insertMessage(msg, chatInfo); this.chatStore.insertMessage(msg, chatInfo);
// //
this.chatStore.insertMessage(msg, chatInfo); if (!friend.isDnd && !this.chatStore.loading &&
if (!friend.isDnd && !this.chatStore.isLoading() &&
!msg.selfSend && msgType.isNormal(msg.type) && !msg.selfSend && msgType.isNormal(msg.type) &&
msg.status != enums.MESSAGE_STATUS.READED) { msg.status != enums.MESSAGE_STATUS.READED) {
this.playAudioTip(); this.playAudioTip();
@ -220,11 +238,6 @@ export default {
type: 'GROUP', type: 'GROUP',
targetId: msg.groupId targetId: msg.groupId
} }
//
if (msg.type == enums.MESSAGE_TYPE.LOADING) {
this.chatStore.setLoadingGroupMsg(JSON.parse(msg.content))
return;
}
// //
if (msg.type == enums.MESSAGE_TYPE.READED) { if (msg.type == enums.MESSAGE_TYPE.READED) {
// //
@ -323,7 +336,7 @@ export default {
// //
this.chatStore.insertMessage(msg, chatInfo); this.chatStore.insertMessage(msg, chatInfo);
// //
if (!group.isDnd && !this.chatStore.isLoading() && if (!group.isDnd && !this.chatStore.loading &&
!msg.selfSend && msgType.isNormal(msg.type) && !msg.selfSend && msgType.isNormal(msg.type) &&
msg.status != enums.MESSAGE_STATUS.READED) { msg.status != enums.MESSAGE_STATUS.READED) {
this.playAudioTip(); this.playAudioTip();

1
im-uniapp/common/request.js

@ -17,6 +17,7 @@ const request = (options) => {
method: options.method || 'GET', method: options.method || 'GET',
header: header, header: header,
data: options.data || {}, data: options.data || {},
timeout: options.timeout || 3000,
async success(res) { async success(res) {
if (res.data.code == 200) { if (res.data.code == 200) {
return resolve(res.data.data) return resolve(res.data.data)

2
im-uniapp/components/chat-item/chat-item.vue

@ -45,7 +45,7 @@ export default {
methods: { methods: {
showChatBox() { showChatBox() {
// //
if (!this.configStore.appInit || this.chatStore.isLoading()) { if (!this.configStore.appInit || this.chatStore.loading) {
uni.showToast({ uni.showToast({
title: "正在初始化页面,请稍后...", title: "正在初始化页面,请稍后...",
icon: 'none' icon: 'none'

2
im-uniapp/components/group-item/group-item.vue

@ -1,6 +1,6 @@
<template> <template>
<view class="group-item" @click="showGroupInfo()"> <view class="group-item" @click="showGroupInfo()">
<head-image :name="group.showGroupName" :url="group.headImage" size="small"></head-image> <head-image :name="group.showGroupName" :url="group.headImageThumb" size="small"></head-image>
<view class="group-name"> <view class="group-name">
<view>{{ group.showGroupName }}</view> <view>{{ group.showGroupName }}</view>
</view> </view>

7
im-uniapp/components/image-upload/image-upload.vue

@ -33,6 +33,10 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
thumbSize: {
type: Number,
default: 50
},
onBefore: { onBefore: {
type: Function, type: Function,
default: null default: null
@ -63,8 +67,9 @@ export default {
}) })
}, },
uploadImage(file) { uploadImage(file) {
let action = `/image/upload?isPermanent=${this.isPermanent}&thumbSize=${this.thumbSize}`
uni.uploadFile({ uni.uploadFile({
url: UNI_APP.BASE_URL + '/image/upload?isPermanent=' + this.isPermanent, url: UNI_APP.BASE_URL + action,
header: { header: {
accessToken: uni.getStorageSync("loginInfo").accessToken accessToken: uni.getStorageSync("loginInfo").accessToken
}, },

1
im-uniapp/main.js

@ -23,6 +23,7 @@ import switchBar from '@/components/bar/switch-bar'
import * as recorder from './common/recorder-h5'; import * as recorder from './common/recorder-h5';
import ImageResize from "quill-image-resize-mp"; import ImageResize from "quill-image-resize-mp";
import Quill from "quill"; import Quill from "quill";
import 'default-passive-events';
// 以下组件用于兼容部分手机聊天边框无法输入的问题 // 以下组件用于兼容部分手机聊天边框无法输入的问题
window.Quill = Quill; window.Quill = Quill;
window.ImageResize = { default: ImageResize }; window.ImageResize = { default: ImageResize };

1
im-uniapp/package.json

@ -4,6 +4,7 @@
"scripts": {} "scripts": {}
}, },
"dependencies": { "dependencies": {
"default-passive-events": "^4.0.0",
"pinyin-pro": "^3.23.1", "pinyin-pro": "^3.23.1",
"quill": "^1.3.7", "quill": "^1.3.7",
"quill-image-resize-mp": "^3.0.1", "quill-image-resize-mp": "^3.0.1",

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

@ -257,12 +257,12 @@ export default {
this.atUserIds = atUserIds; this.atUserIds = atUserIds;
}, },
onLongPressHead(msgInfo) { onLongPressHead(msgInfo) {
if (!msgInfo.selfSend && this.chat.type == "GROUP" && this.atUserIds.indexOf(msgInfo.sendId) < 0) { if (!msgInfo.selfSend && this.isGroup && this.atUserIds.indexOf(msgInfo.sendId) < 0) {
this.atUserIds.push(msgInfo.sendId); this.atUserIds.push(msgInfo.sendId);
} }
}, },
headImage(msgInfo) { headImage(msgInfo) {
if (this.chat.type == 'GROUP') { if (this.isGroup) {
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId); let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
return member ? member.headImage : ""; return member ? member.headImage : "";
} else { } else {
@ -270,7 +270,7 @@ export default {
} }
}, },
showName(msgInfo) { showName(msgInfo) {
if (this.chat.type == 'GROUP') { if (this.isGroup) {
let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId); let member = this.groupMembers.find((m) => m.userId == msgInfo.sendId);
return member ? member.showNickName : ""; return member ? member.showNickName : "";
} else { } else {
@ -325,7 +325,6 @@ export default {
let tmpMessage = this.buildTmpMessage(msgInfo); let tmpMessage = this.buildTmpMessage(msgInfo);
this.chatStore.insertMessage(tmpMessage, chat); this.chatStore.insertMessage(tmpMessage, chat);
this.moveChatToTop(); this.moveChatToTop();
this.sendMessageRequest(msgInfo).then((m) => { this.sendMessageRequest(msgInfo).then((m) => {
// //
tmpMessage = JSON.parse(JSON.stringify(tmpMessage)); tmpMessage = JSON.parse(JSON.stringify(tmpMessage));
@ -358,7 +357,7 @@ export default {
return atText; return atText;
}, },
fillTargetId(msgInfo, targetId) { fillTargetId(msgInfo, targetId) {
if (this.chat.type == "GROUP") { if (this.isGroup) {
msgInfo.groupId = targetId; msgInfo.groupId = targetId;
} else { } else {
msgInfo.recvId = targetId; msgInfo.recvId = targetId;
@ -542,7 +541,7 @@ export default {
// //
this.chatStore.deleteMessage(msgInfo, chat); this.chatStore.deleteMessage(msgInfo, chat);
// //
msgInfo.temId = this.generateId(); msgInfo.tmpId = this.generateId();
let tmpMessage = this.buildTmpMessage(msgInfo); let tmpMessage = this.buildTmpMessage(msgInfo);
this.chatStore.insertMessage(tmpMessage, chat); this.chatStore.insertMessage(tmpMessage, chat);
this.moveChatToTop(); this.moveChatToTop();
@ -676,7 +675,7 @@ export default {
}, 50) }, 50)
}, },
onShowMore() { onShowMore() {
if (this.chat.type == "GROUP") { if (this.isGroup) {
uni.navigateTo({ uni.navigateTo({
url: "/pages/group/group-info?id=" + this.group.id url: "/pages/group/group-info?id=" + this.group.id
}) })
@ -729,7 +728,7 @@ export default {
readedMessage() { readedMessage() {
if (this.unreadCount > 0) { if (this.unreadCount > 0) {
let url = "" let url = ""
if (this.chat.type == "GROUP") { if (this.isGroup) {
url = `/message/group/readed?groupId=${this.chat.targetId}` url = `/message/group/readed?groupId=${this.chat.targetId}`
} else { } else {
url = `/message/private/readed?friendId=${this.chat.targetId}` url = `/message/private/readed?friendId=${this.chat.targetId}`
@ -920,7 +919,7 @@ export default {
sendTime: new Date().getTime(), sendTime: new Date().getTime(),
type: this.$enums.MESSAGE_TYPE.TIP_TEXT type: this.$enums.MESSAGE_TYPE.TIP_TEXT
} }
if (this.chat.type == "PRIVATE") { if (this.isPrivate) {
msgInfo.recvId = this.mine.id msgInfo.recvId = this.mine.id
msgInfo.content = "该用户已被管理员封禁,原因:" + this.userInfo.reason msgInfo.content = "该用户已被管理员封禁,原因:" + this.userInfo.reason
} else { } else {
@ -970,7 +969,7 @@ export default {
return ""; return "";
} }
let title = this.chat.showName; let title = this.chat.showName;
if (this.chat.type == "GROUP") { if (this.isGroup) {
let size = this.groupMembers.filter(m => !m.quit).length; let size = this.groupMembers.filter(m => !m.quit).length;
title += `(${size})`; title += `(${size})`;
} }
@ -992,8 +991,8 @@ export default {
return this.chat.unreadCount; return this.chat.unreadCount;
}, },
isBanned() { isBanned() {
return (this.chat.type == "PRIVATE" && this.userInfo.isBanned) || return (this.isPrivate && this.userInfo.isBanned) ||
(this.chat.type == "GROUP" && this.group.isBanned) (this.isGroup && this.group.isBanned)
}, },
atUserItems() { atUserItems() {
let atUsers = []; let atUsers = [];
@ -1014,6 +1013,15 @@ export default {
}, },
memberSize() { memberSize() {
return this.groupMembers.filter(m => !m.quit).length; return this.groupMembers.filter(m => !m.quit).length;
},
isGroup() {
return this.chat.type == 'GROUP';
},
isPrivate() {
return this.chat.type == 'PRIVATE';
},
loading() {
return this.chatStore.loading;
} }
}, },
watch: { watch: {
@ -1039,6 +1047,14 @@ export default {
this.readedMessage() this.readedMessage()
} }
} }
},
loading: {
handler(newLoading, oldLoading) {
// 线
if (!newLoading && this.isPrivate) {
this.loadReaded(this.chat.targetId)
}
}
} }
}, },
onLoad(options) { onLoad(options) {
@ -1050,7 +1066,7 @@ export default {
// //
this.readedMessage() this.readedMessage()
// //
if (this.chat.type == "GROUP") { if (this.isGroup) {
this.loadGroup(this.chat.targetId); this.loadGroup(this.chat.targetId);
} else { } else {
this.loadFriend(this.chat.targetId); this.loadFriend(this.chat.targetId);

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

@ -111,7 +111,7 @@ export default {
return count; return count;
}, },
loading() { loading() {
return this.chatStore.isLoading(); return this.chatStore.loading;
}, },
initializing() { initializing() {
return !this.configStore.appInit; return !this.configStore.appInit;

2
im-uniapp/pages/common/user-info.vue

@ -61,7 +61,7 @@ export default {
type: 'PRIVATE', type: 'PRIVATE',
targetId: this.userInfo.id, targetId: this.userInfo.id,
showName: this.userInfo.nickName, showName: this.userInfo.nickName,
headImage: this.userInfo.headImage, headImage: this.userInfo.headImageThumb,
}; };
if (this.isFriend) { if (this.isFriend) {
chat.isDnd = this.friendInfo.isDnd; chat.isDnd = this.friendInfo.isDnd;

2
im-uniapp/pages/friend/friend-add.vue

@ -60,7 +60,7 @@ export default {
let friend = { let friend = {
id: user.id, id: user.id,
nickName: user.nickName, nickName: user.nickName,
headImage: user.headImage, headImage: user.headImageThumb,
online: user.online, online: user.online,
delete: false delete: false
} }

11
im-uniapp/pages/friend/friend.vue

@ -12,7 +12,7 @@
温馨提示您现在还没有任何好友快点击右上方'+'按钮添加好友吧~ 温馨提示您现在还没有任何好友快点击右上方'+'按钮添加好友吧~
</view> </view>
<view class="friend-items" v-else> <view class="friend-items" v-else>
<up-index-list :index-list="friendIdx"> <up-index-list :index-list="friendIdx" :sticky="false" :custom-nav-height="50">
<template v-for="(friends, i) in friendGroups"> <template v-for="(friends, i) in friendGroups">
<up-index-item> <up-index-item>
<up-index-anchor :text="friendIdx[i] == '*' ? '在线' : friendIdx[i]"></up-index-anchor> <up-index-anchor :text="friendIdx[i] == '*' ? '在线' : friendIdx[i]"></up-index-anchor>
@ -118,15 +118,6 @@ export default {
color: $im-text-color !important; color: $im-text-color !important;
} }
:deep(.u-index-list__letter__item) {
width: 40rpx !important;
height: 40rpx !important;
}
:deep(.u-index-list__letter__item__index) {
font-size: $im-font-size-small !important;
}
.friend-tip { .friend-tip {
position: absolute; position: absolute;
top: 400rpx; top: 400rpx;

36
im-uniapp/pages/group/group-edit.vue

@ -1,11 +1,11 @@
<template> <template>
<view class="page group-edit"> <view class="page group-edit">
<nav-bar back>修改群资料</nav-bar> <nav-bar back>修改群资料</nav-bar>
<view class="form"> <view class="form">
<view class="form-item"> <view class="form-item">
<view class="label">群聊头像</view> <view class="label">群聊头像</view>
<view class="value"></view> <view class="value"></view>
<image-upload v-if="isOwner" :isPermanent="true" :onSuccess="onUnloadImageSuccess"> <image-upload v-if="isOwner" :isPermanent="true" :thumbSize="20" :onSuccess="onUnloadImageSuccess">
<image :src="group.headImageThumb" class="group-image"></image> <image :src="group.headImageThumb" class="group-image"></image>
</image-upload> </image-upload>
<head-image v-else class="group-image" :name="group.showGroupName" :url="group.headImageThumb" <head-image v-else class="group-image" :name="group.showGroupName" :url="group.headImageThumb"
@ -13,21 +13,24 @@
</view> </view>
<view class="form-item"> <view class="form-item">
<view class="label">群聊名称</view> <view class="label">群聊名称</view>
<input class="input" :class="isOwner?'':'disable'" maxlength="20" v-model="group.name" :disabled="!isOwner" placeholder="请输入群聊名称"/> <input class="input" :class="isOwner?'':'disable'" maxlength="20" v-model="group.name"
:disabled="!isOwner" placeholder="请输入群聊名称" />
</view> </view>
<view class="form-item"> <view class="form-item">
<view class="label">群聊备注</view> <view class="label">群聊备注</view>
<input class="input" maxlength="20" v-model="group.remarkGroupName" :placeholder="group.name"/> <input class="input" maxlength="20" v-model="group.remarkGroupName" :placeholder="group.name" />
</view> </view>
<view class="form-item"> <view class="form-item">
<view class="label">我在本群的昵称</view> <view class="label">我在本群的昵称</view>
<input class="input" maxlength="20" v-model="group.remarkNickName" :placeholder="userStore.userInfo.nickName"/> <input class="input" maxlength="20" v-model="group.remarkNickName"
:placeholder="userStore.userInfo.nickName" />
</view> </view>
<view class="form-item"> <view class="form-item">
<view class="label">群公告</view> <view class="label">群公告</view>
<textarea class="notice" :class="isOwner?'':'disable'" maxlength="512" :disabled="!isOwner" v-model="group.notice" :placeholder="isOwner?'请输入群公告':''"></textarea> <textarea class="notice" :class="isOwner?'':'disable'" maxlength="512" :disabled="!isOwner"
v-model="group.notice" :placeholder="isOwner?'请输入群公告':''"></textarea>
</view> </view>
</view> </view>
<button class="bottom-btn" type="primary" @click="submit()">提交</button> <button class="bottom-btn" type="primary" @click="submit()">提交</button>
</view> </view>
</template> </template>
@ -147,43 +150,43 @@ export default {
.form { .form {
margin-top: 20rpx; margin-top: 20rpx;
.form-item { .form-item {
padding: 0 40rpx; padding: 0 40rpx;
display: flex; display: flex;
background: white; background: white;
align-items: center; align-items: center;
margin-bottom: 2rpx; margin-bottom: 2rpx;
.label { .label {
width: 220rpx; width: 220rpx;
line-height: 100rpx; line-height: 100rpx;
font-size: $im-font-size; font-size: $im-font-size;
white-space: nowrap; white-space: nowrap;
} }
.value{ .value {
flex: 1; flex: 1;
} }
.input { .input {
flex: 1; flex: 1;
text-align: right; text-align: right;
line-height: 100rpx; line-height: 100rpx;
font-size: $im-font-size-small; font-size: $im-font-size-small;
} }
.disable { .disable {
color: $im-text-color-lighter; color: $im-text-color-lighter;
} }
.notice { .notice {
flex: 1; flex: 1;
font-size: $im-font-size-small; font-size: $im-font-size-small;
max-height: 200rpx; max-height: 200rpx;
padding: 14rpx 0; padding: 14rpx 0;
} }
.group-image { .group-image {
width: 120rpx; width: 120rpx;
height: 120rpx; height: 120rpx;
@ -193,5 +196,4 @@ export default {
} }
} }
} }
</style>
</style>

2
im-uniapp/pages/group/group-info.vue

@ -122,7 +122,7 @@ export default {
type: 'GROUP', type: 'GROUP',
targetId: this.group.id, targetId: this.group.id,
showName: this.group.showGroupName, showName: this.group.showGroupName,
headImage: this.group.headImage, headImage: this.group.headImageThumb,
isDnd: this.group.isDnd isDnd: this.group.isDnd
}; };
this.chatStore.openChat(chat); this.chatStore.openChat(chat);

2
im-uniapp/pages/mine/mine-edit.vue

@ -4,7 +4,7 @@
<view class="form"> <view class="form">
<view class="form-item"> <view class="form-item">
<view class="label">头像</view> <view class="label">头像</view>
<image-upload class="value" :isPermanent="true" :onSuccess="onUnloadImageSuccess"> <image-upload class="value" :isPermanent="true" :thumbSize="20" :onSuccess="onUnloadImageSuccess">
<image :src="userInfo.headImageThumb" class="head-image"></image> <image :src="userInfo.headImageThumb" class="head-image"></image>
</image-upload> </image-upload>
</view> </view>

86
im-uniapp/store/chatStore.js

@ -3,7 +3,6 @@ import { MESSAGE_TYPE, MESSAGE_STATUS } from '@/common/enums.js';
import useFriendStore from './friendStore.js'; import useFriendStore from './friendStore.js';
import useGroupStore from './groupStore.js'; import useGroupStore from './groupStore.js';
import useUserStore from './userStore'; import useUserStore from './userStore';
import useConfigStore from './configStore.js';
let cacheChats = []; let cacheChats = [];
export default defineStore('chatStore', { export default defineStore('chatStore', {
@ -12,8 +11,7 @@ export default defineStore('chatStore', {
chats: [], chats: [],
privateMsgMaxId: 0, privateMsgMaxId: 0,
groupMsgMaxId: 0, groupMsgMaxId: 0,
loadingPrivateMsg: false, loading: false
loadingGroupMsg: false
} }
}, },
actions: { actions: {
@ -140,7 +138,7 @@ export default defineStore('chatStore', {
} }
}, },
moveTop(idx) { moveTop(idx) {
if (this.isLoading()) { if (this.loading) {
return; return;
} }
let chats = this.curChats; let chats = this.curChats;
@ -156,20 +154,18 @@ export default defineStore('chatStore', {
insertMessage(msgInfo, chatInfo) { insertMessage(msgInfo, chatInfo) {
// 获取对方id或群id // 获取对方id或群id
let type = chatInfo.type; let type = chatInfo.type;
// 完成初始化之前不能修改消息最大id,否则可能导致拉不到离线消息 // 记录消息的最大id
if (useConfigStore().appInit) { if (msgInfo.id && type == "PRIVATE" && msgInfo.id > this.privateMsgMaxId) {
// 记录消息的最大id this.privateMsgMaxId = msgInfo.id;
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > this.privateMsgMaxId) { }
this.privateMsgMaxId = msgInfo.id; if (msgInfo.id && type == "GROUP" && msgInfo.id > this.groupMsgMaxId) {
} this.groupMsgMaxId = msgInfo.id;
if (msgInfo.id && type == "GROUP" && msgInfo.id > this.groupMsgMaxId) {
this.groupMsgMaxId = msgInfo.id;
}
} }
// 如果是已存在消息,则覆盖旧的消息数据 // 如果是已存在消息,则覆盖旧的消息数据
let chat = this.findChat(chatInfo); let chat = this.findChat(chatInfo);
let message = this.findMessage(chat, msgInfo); let message = this.findMessage(chat, msgInfo);
if (message) { if (message) {
console.log("message:", message)
Object.assign(message, msgInfo); Object.assign(message, msgInfo);
chat.stored = false; chat.stored = false;
this.saveToStorage(); this.saveToStorage();
@ -218,24 +214,8 @@ export default defineStore('chatStore', {
}); });
chat.lastTimeTip = msgInfo.sendTime; chat.lastTimeTip = msgInfo.sendTime;
} }
// 根据id顺序插入,防止消息乱序 // 插入消息
let insertPos = chat.messages.length; chat.messages.push(msgInfo);
// 防止 图片、文件 在发送方 显示 在顶端 因为还没存库,id=0
if (msgInfo.id && msgInfo.id > 0) {
for (let idx in chat.messages) {
if (chat.messages[idx].id && msgInfo.id < chat.messages[idx].id) {
insertPos = idx;
console.log(`消息出现乱序,位置:${chat.messages.length},修正至:${insertPos}`);
break;
}
}
}
if (insertPos == chat.messages.length) {
// 这种赋值效率最高
chat.messages[insertPos] = msgInfo;
} else {
chat.messages.splice(insertPos, 0, msgInfo);
}
chat.stored = false; chat.stored = false;
this.saveToStorage(); this.saveToStorage();
}, },
@ -345,17 +325,8 @@ export default defineStore('chatStore', {
this.saveToStorage(); this.saveToStorage();
} }
}, },
setLoadingPrivateMsg(loading) { setLoading(loading) {
this.loadingPrivateMsg = loading; this.loading = loading;
if (!this.isLoading()) {
this.refreshChats()
}
},
setLoadingGroupMsg(loading) {
this.loadingGroupMsg = loading;
if (!this.isLoading()) {
this.refreshChats()
}
}, },
setDnd(chatInfo, isDnd) { setDnd(chatInfo, isDnd) {
let chat = this.findChat(chatInfo); let chat = this.findChat(chatInfo);
@ -406,7 +377,7 @@ export default defineStore('chatStore', {
}, },
saveToStorage(withColdMessage) { saveToStorage(withColdMessage) {
// 加载中不保存,防止卡顿 // 加载中不保存,防止卡顿
if (this.isLoading()) { if (this.loading) {
return; return;
} }
const userStore = useUserStore(); const userStore = useUserStore();
@ -493,11 +464,8 @@ export default defineStore('chatStore', {
} }
}, },
getters: { getters: {
isLoading: (state) => () => {
return state.loadingPrivateMsg || state.loadingGroupMsg
},
curChats: (state) => { curChats: (state) => {
if (cacheChats && state.isLoading()) { if (cacheChats && state.loading) {
return cacheChats; return cacheChats;
} }
return state.chats; return state.chats;
@ -529,17 +497,29 @@ export default defineStore('chatStore', {
if (!chat) { if (!chat) {
return null; return null;
} }
for (let idx in chat.messages) { for (let idx = chat.messages.length - 1; idx >= 0; idx--) {
// 通过id判断 // 通过id判断
if (msgInfo.id && chat.messages[idx].id == msgInfo.id) { if (msgInfo.id && chat.messages[idx].id) {
return chat.messages[idx]; if (msgInfo.id == chat.messages[idx].id) {
return chat.messages[idx];
}
// 如果id比要查询的消息小,说明没有这条消息
if (msgInfo.id > chat.messages[idx].id) {
break;
}
} }
// 正在发送中的消息可能没有id,只有tmpId // 正在发送中的消息可能没有id,只有tmpId
if (msgInfo.tmpId && chat.messages[idx].tmpId && if (msgInfo.tmpId && chat.messages[idx].tmpId) {
chat.messages[idx].tmpId == msgInfo.tmpId) { if (msgInfo.tmpId == chat.messages[idx].tmpId) {
return chat.messages[idx]; return chat.messages[idx];
}
// 如果id比要查询的消息小,说明没有这条消息
if (msgInfo.tmpId > chat.messages[idx].tmpId) {
break;
}
} }
} }
return null;
} }
} }
}); });

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

@ -784,6 +784,9 @@ export default {
}, },
isPrivate() { isPrivate() {
return this.chat.type == 'PRIVATE'; return this.chat.type == 'PRIVATE';
},
loading() {
return this.chatStore.loading;
} }
}, },
watch: { watch: {
@ -833,6 +836,14 @@ export default {
} }
} }
} }
},
loading: {
handler(newLoading, oldLoading) {
// 线
if (!newLoading && this.isPrivate) {
this.loadReaded(this.chat.targetId)
}
}
} }
}, },
mounted() { mounted() {

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

@ -320,7 +320,7 @@ export default {
.message-image { .message-image {
border-radius: 8px; border-radius: 8px;
border: 3px solid var(--im-color-primary-light-8); border: 2px solid var(--im-color-primary-light-9);
overflow: hidden; overflow: hidden;
cursor: pointer; cursor: pointer;
} }

5
im-web/src/components/common/FileUpload.vue

@ -55,8 +55,11 @@ export default {
} }
let formData = new FormData() let formData = new FormData()
formData.append('file', file.file) formData.append('file', file.file)
let url = this.action;
url += this.action.includes("?") ? "&" : "?"
url += 'isPermanent=' + this.isPermanent;
this.$http({ this.$http({
url: this.action + '?isPermanent=' + this.isPermanent, url: url,
data: formData, data: formData,
method: 'post', method: 'post',
headers: { headers: {

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

@ -65,7 +65,7 @@ export default {
type: 'PRIVATE', type: 'PRIVATE',
targetId: user.id, targetId: user.id,
showName: user.nickName, showName: user.nickName,
headImage: user.headImage headImage: user.headImageThumb
}; };
if (this.isFriend) { if (this.isFriend) {
chat.isDnd = this.friendInfo.isDnd; chat.isDnd = this.friendInfo.isDnd;

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

@ -79,7 +79,7 @@ export default {
let friend = { let friend = {
id: user.id, id: user.id,
nickName: user.nickName, nickName: user.nickName,
headImage: user.headImage, headImage: user.headImageThumb,
online: user.online, online: user.online,
deleted: false deleted: false
} }

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

@ -1,7 +1,7 @@
<template> <template>
<div class="group-item" :class="active ? 'active' : ''"> <div class="group-item" :class="active ? 'active' : ''">
<div class="group-avatar"> <div class="group-avatar">
<head-image :size="42" :name="group.showGroupName" :url="group.headImage"> </head-image> <head-image :size="42" :name="group.showGroupName" :url="group.headImageThumb"> </head-image>
</div> </div>
<div class="group-name"> <div class="group-name">
<div>{{ group.showGroupName }}</div> <div>{{ group.showGroupName }}</div>

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

@ -87,7 +87,7 @@ export default {
}, },
computed: { computed: {
imageAction() { imageAction() {
return `/image/upload`; return `/image/upload?thumbSize=20`;
} }
}, },
watch: { watch: {

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

@ -3,7 +3,6 @@ import { MESSAGE_TYPE, MESSAGE_STATUS } from "../api/enums.js"
import useFriendStore from './friendStore.js'; import useFriendStore from './friendStore.js';
import useGroupStore from './groupStore.js'; import useGroupStore from './groupStore.js';
import useUserStore from './userStore.js'; import useUserStore from './userStore.js';
import useConfigStore from './configStore.js';
import localForage from 'localforage'; import localForage from 'localforage';
/** /**
@ -28,11 +27,10 @@ export default defineStore('chatStore', {
state: () => { state: () => {
return { return {
activeChat: null, activeChat: null,
chats: [],
privateMsgMaxId: 0, privateMsgMaxId: 0,
groupMsgMaxId: 0, groupMsgMaxId: 0,
loadingPrivateMsg: false, loading: false
loadingGroupMsg: false,
chats: []
} }
}, },
actions: { actions: {
@ -141,7 +139,7 @@ export default defineStore('chatStore', {
}, },
moveTop(idx) { moveTop(idx) {
// 加载中不移动,很耗性能 // 加载中不移动,很耗性能
if (this.isLoading()) { if (this.loading) {
return; return;
} }
if (idx > 0) { if (idx > 0) {
@ -156,15 +154,12 @@ export default defineStore('chatStore', {
}, },
insertMessage(msgInfo, chatInfo) { insertMessage(msgInfo, chatInfo) {
let type = chatInfo.type; let type = chatInfo.type;
// 完成初始化之前不能修改消息最大id,否则可能导致拉不到离线消息 // 记录消息的最大id
if (useConfigStore().appInit) { if (msgInfo.id && type == "PRIVATE" && msgInfo.id > this.privateMsgMaxId) {
// 记录消息的最大id this.privateMsgMaxId = msgInfo.id;
if (msgInfo.id && type == "PRIVATE" && msgInfo.id > this.privateMsgMaxId) { }
this.privateMsgMaxId = msgInfo.id; if (msgInfo.id && type == "GROUP" && msgInfo.id > this.groupMsgMaxId) {
} this.groupMsgMaxId = msgInfo.id;
if (msgInfo.id && type == "GROUP" && msgInfo.id > this.groupMsgMaxId) {
this.groupMsgMaxId = msgInfo.id;
}
} }
// 如果是已存在消息,则覆盖旧的消息数据 // 如果是已存在消息,则覆盖旧的消息数据
let chat = this.findChat(chatInfo); let chat = this.findChat(chatInfo);
@ -218,19 +213,7 @@ export default defineStore('chatStore', {
}); });
chat.lastTimeTip = msgInfo.sendTime; chat.lastTimeTip = msgInfo.sendTime;
} }
// 根据id顺序插入,防止消息乱序 chat.messages.push(msgInfo);
let insertPos = chat.messages.length;
// 防止 图片、文件 在发送方 显示 在顶端 因为还没存库,id=0
if (msgInfo.id && msgInfo.id > 0) {
for (let idx in chat.messages) {
if (chat.messages[idx].id && msgInfo.id < chat.messages[idx].id) {
insertPos = idx;
console.log(`消息出现乱序,位置:${chat.messages.length},修正至:${insertPos}`);
break;
}
}
}
chat.messages.splice(insertPos, 0, msgInfo);
chat.stored = false; chat.stored = false;
this.saveToStorage(); this.saveToStorage();
}, },
@ -340,17 +323,8 @@ export default defineStore('chatStore', {
this.saveToStorage() this.saveToStorage()
} }
}, },
setLoadingPrivateMsg(loading) { setLoading(loading) {
this.loadingPrivateMsg = loading; this.loading = loading;
if (!this.isLoading()) {
this.refreshChats();
}
},
setLoadingGroupMsg(loading) {
this.loadingGroupMsg = loading;
if (!this.isLoading()) {
this.refreshChats();
}
}, },
setDnd(chatInfo, isDnd) { setDnd(chatInfo, isDnd) {
let chat = this.findChat(chatInfo); let chat = this.findChat(chatInfo);
@ -399,7 +373,7 @@ export default defineStore('chatStore', {
}, },
saveToStorage(withColdMessage) { saveToStorage(withColdMessage) {
// 加载中不保存,防止卡顿 // 加载中不保存,防止卡顿
if (this.isLoading()) { if (this.loading) {
return; return;
} }
const userStore = useUserStore(); const userStore = useUserStore();
@ -456,6 +430,9 @@ export default defineStore('chatStore', {
cacheChats = [] cacheChats = []
this.chats = []; this.chats = [];
this.activeChat = null; this.activeChat = null;
this.privateMsgMaxId = 0;
this.groupMsgMaxId = 0;
this.loading = false;
}, },
loadChat() { loadChat() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -511,7 +488,7 @@ export default defineStore('chatStore', {
return state.loadingPrivateMsg || state.loadingGroupMsg return state.loadingPrivateMsg || state.loadingGroupMsg
}, },
findChats: (state) => () => { findChats: (state) => () => {
if (cacheChats && state.isLoading()) { if (cacheChats && state.loading) {
return cacheChats; return cacheChats;
} }
return state.chats; return state.chats;
@ -545,15 +522,26 @@ export default defineStore('chatStore', {
if (!chat) { if (!chat) {
return null; return null;
} }
for (let idx in chat.messages) { for (let idx = chat.messages.length - 1; idx >= 0; idx--) {
// 通过id判断 // 通过id判断
if (msgInfo.id && chat.messages[idx].id == msgInfo.id) { if (msgInfo.id && chat.messages[idx].id) {
return chat.messages[idx]; if (msgInfo.id == chat.messages[idx].id) {
return chat.messages[idx];
}
// 如果id比要查询的消息小,说明没有这条消息
if (msgInfo.id > chat.messages[idx].id) {
break;
}
} }
// 正在发送中的消息可能没有id,只有tmpId // 正在发送中的消息可能没有id,只有tmpId
if (msgInfo.tmpId && chat.messages[idx].tmpId && if (msgInfo.tmpId && chat.messages[idx].tmpId) {
chat.messages[idx].tmpId == msgInfo.tmpId) { if (msgInfo.tmpId == chat.messages[idx].tmpId) {
return chat.messages[idx]; return chat.messages[idx];
}
// 如果id比要查询的消息小,说明没有这条消息
if (msgInfo.tmpId > chat.messages[idx].tmpId) {
break;
}
} }
} }
} }

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

@ -33,14 +33,36 @@ export default defineStore('friendStore', {
this.friends.unshift(friend); this.friends.unshift(friend);
} }
}, },
updateOnlineStatus(onlineData) { setOnlineStatus(onlineTerminals) {
let friend = this.findFriend(onlineData.userId); this.friends.forEach((f) => {
if (onlineData.terminal == TERMINAL_TYPE.WEB) { let userTerminal = onlineTerminals.find((o) => f.id == o.userId);
friend.onlineWeb = onlineData.online; if (userTerminal) {
} else if (onlineData.terminal == TERMINAL_TYPE.APP) { f.online = true;
friend.onlineApp = onlineData.online; 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;
}
});
},
refreshOnlineStatus() {
let userIds = this.friends.filter((f) => !f.deleted).map((f) => f.id);
if (userIds.length == 0) {
return;
} }
friend.online = friend.onlineWeb || friend.onlineApp; http({
url: '/user/terminal/online?userIds=' + userIds.join(','),
method: 'GET'
}).then((onlineTerminals) => {
this.setOnlineStatus(onlineTerminals);
})
// 30s后重新拉取
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.refreshOnlineStatus();
}, 30000)
}, },
setDnd(id, isDnd) { setDnd(id, isDnd) {
let friend = this.findFriend(id); let friend = this.findFriend(id);
@ -58,6 +80,7 @@ export default defineStore('friendStore', {
method: 'GET' method: 'GET'
}).then(async (friends) => { }).then(async (friends) => {
this.setFriends(friends); this.setFriends(friends);
this.refreshOnlineStatus();
resolve(); resolve();
}).catch(e => { }).catch(e => {
reject(e); reject(e);

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

@ -89,7 +89,7 @@ export default {
}, },
computed: { computed: {
loading() { loading() {
return this.chatStore.loadingGroupMsg || this.chatStore.loadingPrivateMsg return this.chatStore.loading;
} }
} }
} }

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

@ -118,7 +118,7 @@ export default {
let friend = { let friend = {
id: user.id, id: user.id,
nickName: user.nickName, nickName: user.nickName,
headImage: user.headImage, headImage: user.headImageThumb,
online: user.online online: user.online
} }
this.friendStore.addFriend(friend); this.friendStore.addFriend(friend);

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

@ -240,7 +240,7 @@ export default {
type: 'GROUP', type: 'GROUP',
targetId: this.activeGroup.id, targetId: this.activeGroup.id,
showName: this.activeGroup.showGroupName, showName: this.activeGroup.showGroupName,
headImage: this.activeGroup.headImage, headImage: this.activeGroup.headImageThumb,
isDnd: this.activeGroup.isDnd isDnd: this.activeGroup.isDnd
}; };
this.chatStore.openChat(chat); this.chatStore.openChat(chat);
@ -295,7 +295,7 @@ export default {
return this.activeGroup.ownerId == this.userStore.userInfo.id; return this.activeGroup.ownerId == this.userStore.userInfo.id;
}, },
imageAction() { imageAction() {
return `/image/upload`; return `/image/upload?thumbSize=20`;
}, },
groupMap() { groupMap() {
// //

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

@ -78,7 +78,9 @@ export default {
showSettingDialog: false, showSettingDialog: false,
lastPlayAudioTime: new Date().getTime() - 1000, lastPlayAudioTime: new Date().getTime() - 1000,
isFullscreen: true, isFullscreen: true,
reconnecting: false reconnecting: false,
privateMessagesBuffer: [],
groupMessagesBuffer: []
} }
}, },
methods: { methods: {
@ -108,8 +110,7 @@ export default {
this.onReconnectWs(); this.onReconnectWs();
} else { } else {
// 线 // 线
this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId); this.pullOfflineMessage();
this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId);
this.configStore.setAppInit(true); this.configStore.setAppInit(true);
} }
}); });
@ -124,13 +125,22 @@ export default {
location.href = "/"; location.href = "/";
} }
}); });
} else if (cmd == 3) { } else if (cmd == 3) {
// if (!this.configStore.appInit || this.chatStore.loading) {
this.handlePrivateMessage(msgInfo); // 线
this.privateMessagesBuffer.push(msgInfo);
} else {
//
this.handlePrivateMessage(msgInfo);
}
} else if (cmd == 4) { } else if (cmd == 4) {
// if (!this.configStore.appInit || this.chatStore.loading) {
this.handleGroupMessage(msgInfo); // 线
this.groupMessagesBuffer.push(msgInfo);
} else {
//
this.handleGroupMessage(msgInfo);
}
} else if (cmd == 5) { } else if (cmd == 5) {
// //
this.handleSystemMessage(msgInfo); this.handleSystemMessage(msgInfo);
@ -170,8 +180,7 @@ export default {
promises.push(this.groupStore.loadGroup()); promises.push(this.groupStore.loadGroup());
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
// 线 // 线
this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId); this.pullOfflineMessage();
this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId);
this.configStore.setAppInit(true) this.configStore.setAppInit(true)
this.$message.success("重新连接成功"); this.$message.success("重新连接成功");
}).catch(() => { }).catch(() => {
@ -194,23 +203,42 @@ export default {
this.groupStore.clear(); this.groupStore.clear();
this.chatStore.clear(); this.chatStore.clear();
this.userStore.clear(); this.userStore.clear();
},
pullOfflineMessage() {
this.chatStore.setLoading(true);
const promises = [];
promises.push(this.pullPrivateOfflineMessage(this.chatStore.privateMsgMaxId));
promises.push(this.pullGroupOfflineMessage(this.chatStore.groupMsgMaxId));
Promise.all(promises).then(messages => {
// 线
messages[0].forEach(m => this.handlePrivateMessage(m));
messages[1].forEach(m => this.handleGroupMessage(m));
//
this.privateMessagesBuffer.forEach(m => this.handlePrivateMessage(m));
this.groupMessagesBuffer.forEach(m => this.handleGroupMessage(m));
//
this.privateMessagesBuffer = [];
this.groupMessagesBuffer = [];
// 线
this.chatStore.setLoading(false);
//
this.chatStore.refreshChats();
}).catch((e) => {
console.log(e)
this.$message.error("拉取离线消息失败");
this.onExit();
})
}, },
pullPrivateOfflineMessage(minId) { pullPrivateOfflineMessage(minId) {
this.chatStore.setLoadingPrivateMsg(true) return this.$http({
this.$http({ url: "/message/private/loadOfflineMessage?minId=" + minId,
url: "/message/private/pullOfflineMessage?minId=" + minId,
method: 'GET' method: 'GET'
}).catch(() => {
this.chatStore.setLoadingPrivateMsg(false)
}) })
}, },
pullGroupOfflineMessage(minId) { pullGroupOfflineMessage(minId) {
this.chatStore.setLoadingGroupMsg(true) return this.$http({
this.$http({ url: "/message/group/loadOfflineMessage?minId=" + minId,
url: "/message/group/pullOfflineMessage?minId=" + minId,
method: 'GET' method: 'GET'
}).catch(() => {
this.chatStore.setLoadingGroupMsg(false)
}) })
}, },
handlePrivateMessage(msg) { handlePrivateMessage(msg) {
@ -223,11 +251,6 @@ export default {
type: 'PRIVATE', type: 'PRIVATE',
targetId: friendId targetId: friendId
} }
//
if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) {
this.chatStore.setLoadingPrivateMsg(JSON.parse(msg.content))
return;
}
// //
if (msg.type == this.$enums.MESSAGE_TYPE.READED) { if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
this.chatStore.resetUnreadCount(chatInfo) this.chatStore.resetUnreadCount(chatInfo)
@ -285,7 +308,7 @@ export default {
// //
this.chatStore.insertMessage(msg, chatInfo); this.chatStore.insertMessage(msg, chatInfo);
// //
if (!friend.isDnd && !this.chatStore.isLoading() && !msg.selfSend && this.$msgType.isNormal(msg.type) && if (!friend.isDnd && !this.chatStore.loading && !msg.selfSend && this.$msgType.isNormal(msg.type) &&
msg.status != this.$enums.MESSAGE_STATUS.READED) { msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip(); this.playAudioTip();
} }
@ -297,11 +320,6 @@ export default {
type: 'GROUP', type: 'GROUP',
targetId: msg.groupId targetId: msg.groupId
} }
//
if (msg.type == this.$enums.MESSAGE_TYPE.LOADING) {
this.chatStore.setLoadingGroupMsg(JSON.parse(msg.content))
return;
}
// //
if (msg.type == this.$enums.MESSAGE_TYPE.READED) { if (msg.type == this.$enums.MESSAGE_TYPE.READED) {
// //
@ -367,7 +385,7 @@ export default {
// //
this.chatStore.insertMessage(msg, chatInfo); this.chatStore.insertMessage(msg, chatInfo);
// //
if (!group.isDnd && !this.chatStore.isLoading() && if (!group.isDnd && !this.chatStore.loading &&
!msg.selfSend && this.$msgType.isNormal(msg.type) !msg.selfSend && this.$msgType.isNormal(msg.type)
&& msg.status != this.$enums.MESSAGE_STATUS.READED) { && msg.status != this.$enums.MESSAGE_STATUS.READED) {
this.playAudioTip(); this.playAudioTip();

Loading…
Cancel
Save