diff --git a/db/im-platform.sql b/db/im-platform.sql index a32c6a2..575610f 100644 --- a/db/im-platform.sql +++ b/db/im-platform.sql @@ -22,6 +22,7 @@ create table `im_friend`( `friend_id` bigint not null comment '好友id', `friend_nick_name` varchar(255) not null comment '好友昵称', `friend_head_image` varchar(255) default '' comment '好友头像', + `is_dnd` tinyint comment '免打扰标识(Do Not Disturb) 0:关闭 1:开启', `deleted` tinyint comment '删除标识 0:正常 1:已删除', `created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间', key `idx_user_id` (`user_id`), @@ -62,6 +63,7 @@ create table `im_group_member`( `remark_nick_name` varchar(255) DEFAULT '' comment '显示昵称备注', `head_image` varchar(255) DEFAULT '' comment '用户头像', `remark_group_name` varchar(255) DEFAULT '' comment '显示群名备注', + `is_dnd` tinyint comment '免打扰标识(Do Not Disturb) 0:关闭 1:开启', `quit` tinyint(1) DEFAULT 0 comment '是否已退出', `quit_time` datetime DEFAULT NULL comment '退出时间', `created_time` datetime DEFAULT CURRENT_TIMESTAMP comment '创建时间', diff --git a/im-platform/src/main/java/com/bx/implatform/controller/FriendController.java b/im-platform/src/main/java/com/bx/implatform/controller/FriendController.java index fae3c61..3b54fa0 100644 --- a/im-platform/src/main/java/com/bx/implatform/controller/FriendController.java +++ b/im-platform/src/main/java/com/bx/implatform/controller/FriendController.java @@ -1,12 +1,14 @@ package com.bx.implatform.controller; import com.bx.implatform.annotation.RepeatSubmit; +import com.bx.implatform.dto.FriendDndDTO; import com.bx.implatform.result.Result; import com.bx.implatform.result.ResultUtils; import com.bx.implatform.service.FriendService; import com.bx.implatform.vo.FriendVO; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -50,5 +52,12 @@ public class FriendController { return ResultUtils.success(); } + @PutMapping("/dnd") + @Operation(summary = "开启/关闭免打扰状态", description = "开启/关闭免打扰状态") + public Result setFriendDnd(@Valid @RequestBody FriendDndDTO dto) { + friendService.setDnd(dto); + return ResultUtils.success(); + } + } diff --git a/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java b/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java index a95b692..af25a5a 100644 --- a/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java +++ b/im-platform/src/main/java/com/bx/implatform/controller/GroupController.java @@ -1,6 +1,7 @@ package com.bx.implatform.controller; import com.bx.implatform.annotation.RepeatSubmit; +import com.bx.implatform.dto.GroupDndDTO; import com.bx.implatform.dto.GroupInviteDTO; import com.bx.implatform.dto.GroupMemberRemoveDTO; import com.bx.implatform.result.Result; @@ -101,5 +102,12 @@ public class GroupController { return ResultUtils.success(); } + @Operation(summary = "开启/关闭免打扰", description = "开启/关闭免打扰") + @PutMapping("/dnd") + public Result setGroupDnd(@Valid @RequestBody GroupDndDTO dto) { + groupService.setDnd(dto); + return ResultUtils.success(); + } + } diff --git a/im-platform/src/main/java/com/bx/implatform/dto/FriendDndDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/FriendDndDTO.java new file mode 100644 index 0000000..12d1721 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/FriendDndDTO.java @@ -0,0 +1,23 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * @author Blue + * @version 1.0 + */ +@Data +@Schema(description = "好友免打扰") +public class FriendDndDTO { + + @NotNull(message = "好友id不可为空") + @Schema(description = "好友用户id") + private Long friendId; + + @NotNull(message = "消息免打扰状态不可为空") + @Schema(description = "消息免打扰状态") + private Boolean isDnd; + +} diff --git a/im-platform/src/main/java/com/bx/implatform/dto/GroupDndDTO.java b/im-platform/src/main/java/com/bx/implatform/dto/GroupDndDTO.java new file mode 100644 index 0000000..1358b82 --- /dev/null +++ b/im-platform/src/main/java/com/bx/implatform/dto/GroupDndDTO.java @@ -0,0 +1,23 @@ +package com.bx.implatform.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +/** + * @author Blue + * @version 1.0 + * @date 2025-02-23 + */ +@Data +@Schema(description = "群聊免打扰") +public class GroupDndDTO { + + @NotNull(message = "群id不可为空") + @Schema(description = "群组id") + private Long groupId; + + @NotNull(message = "免打扰状态不可为空") + @Schema(description = "免打扰状态") + private Boolean isDnd; +} diff --git a/im-platform/src/main/java/com/bx/implatform/entity/Friend.java b/im-platform/src/main/java/com/bx/implatform/entity/Friend.java index c7451f9..e3f96f7 100644 --- a/im-platform/src/main/java/com/bx/implatform/entity/Friend.java +++ b/im-platform/src/main/java/com/bx/implatform/entity/Friend.java @@ -45,6 +45,11 @@ public class Friend{ */ private String friendHeadImage; + /** + * 是否开启免打扰 + */ + private Boolean isDnd; + /** * 是否已删除 */ diff --git a/im-platform/src/main/java/com/bx/implatform/entity/GroupMember.java b/im-platform/src/main/java/com/bx/implatform/entity/GroupMember.java index 415eb83..8aec5cc 100644 --- a/im-platform/src/main/java/com/bx/implatform/entity/GroupMember.java +++ b/im-platform/src/main/java/com/bx/implatform/entity/GroupMember.java @@ -58,6 +58,12 @@ public class GroupMember extends Model { */ private String remarkGroupName; + /** + * 是否免打扰 + */ + private Boolean isDnd; + + /** * 是否已退出 */ diff --git a/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java b/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java index 6d12868..b044a44 100644 --- a/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java +++ b/im-platform/src/main/java/com/bx/implatform/enums/MessageType.java @@ -37,8 +37,10 @@ public enum MessageType { GROUP_UNBAN(52,"群聊解封"), FRIEND_NEW(80, "新增好友"), FRIEND_DEL(81, "删除好友"), + FRIEND_DND(82, "好友免打扰"), GROUP_NEW(90, "新增群聊"), GROUP_DEL(91, "删除群聊"), + GROUP_DND(92, "群聊免打扰"), RTC_CALL_VOICE(100, "语音呼叫"), RTC_CALL_VIDEO(101, "视频呼叫"), RTC_ACCEPT(102, "接受"), diff --git a/im-platform/src/main/java/com/bx/implatform/service/FriendService.java b/im-platform/src/main/java/com/bx/implatform/service/FriendService.java index 70088a4..282a090 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/FriendService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/FriendService.java @@ -1,6 +1,7 @@ package com.bx.implatform.service; import com.baomidou.mybatisplus.extension.service.IService; +import com.bx.implatform.dto.FriendDndDTO; import com.bx.implatform.entity.Friend; import com.bx.implatform.vo.FriendVO; @@ -70,4 +71,10 @@ public interface FriendService extends IService { */ void bindFriend(Long userId, Long friendId); + /** + * 设置好友免打扰状态 + * @param dto + */ + void setDnd(FriendDndDTO dto); + } \ No newline at end of file diff --git a/im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java b/im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java index dd0ae42..770943b 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/GroupMemberService.java @@ -1,6 +1,7 @@ package com.bx.implatform.service; import com.baomidou.mybatisplus.extension.service.IService; +import com.bx.implatform.dto.GroupDndDTO; import com.bx.implatform.entity.GroupMember; import java.util.List; @@ -90,4 +91,12 @@ public interface GroupMemberService extends IService { * @param userIds 用户id */ Boolean isInGroup(Long groupId,List userIds); + + /** + * 设置免打扰状态 + * @param groupId 群id + * @param userId 用户id + * @param isDnd 是否开启免打扰 + */ + void setDnd(Long groupId, Long userId, Boolean isDnd); } diff --git a/im-platform/src/main/java/com/bx/implatform/service/GroupService.java b/im-platform/src/main/java/com/bx/implatform/service/GroupService.java index e471b7d..a43320a 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/GroupService.java +++ b/im-platform/src/main/java/com/bx/implatform/service/GroupService.java @@ -1,6 +1,7 @@ package com.bx.implatform.service; import com.baomidou.mybatisplus.extension.service.IService; +import com.bx.implatform.dto.GroupDndDTO; import com.bx.implatform.dto.GroupInviteDTO; import com.bx.implatform.dto.GroupMemberRemoveDTO; import com.bx.implatform.entity.Group; @@ -92,4 +93,10 @@ public interface GroupService extends IService { * @return List **/ List findGroupMembers(Long groupId); + + /** + * 开启/关闭免打扰 + * @param dto + */ + void setDnd(GroupDndDTO dto); } diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java index a302596..bbe6204 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/FriendServiceImpl.java @@ -11,6 +11,7 @@ import com.bx.imcommon.enums.IMTerminalType; import com.bx.imcommon.model.IMPrivateMessage; import com.bx.imcommon.model.IMUserInfo; import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.dto.FriendDndDTO; import com.bx.implatform.entity.Friend; import com.bx.implatform.entity.PrivateMessage; import com.bx.implatform.entity.User; @@ -138,6 +139,18 @@ public class FriendServiceImpl extends ServiceImpl impleme sendAddFriendMessage(userId, friendId, friend); } + @Override + public void setDnd(FriendDndDTO dto) { + UserSession session = SessionContext.getSession(); + LambdaUpdateWrapper wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(Friend::getUserId, session.getUserId()); + wrapper.eq(Friend::getFriendId, dto.getFriendId()); + wrapper.set(Friend::getIsDnd, dto.getIsDnd()); + this.update(wrapper); + // 推送同步消息 + sendSyncDndMessage(dto.getFriendId(), dto.getIsDnd()); + } + /** * 单向解除好友关系 * @@ -175,6 +188,7 @@ public class FriendServiceImpl extends ServiceImpl impleme vo.setHeadImage(f.getFriendHeadImage()); vo.setNickName(f.getFriendNickName()); vo.setDeleted(f.getDeleted()); + vo.setIsDnd(f.getIsDnd()); return vo; } @@ -254,4 +268,21 @@ public class FriendServiceImpl extends ServiceImpl impleme sendMessage.setData(messageInfo); imClient.sendPrivateMessage(sendMessage); } + + void sendSyncDndMessage(Long friendId, Boolean isDnd) { + // 同步免打扰状态到其他终端 + UserSession session = SessionContext.getSession(); + PrivateMessageVO msgInfo = new PrivateMessageVO(); + msgInfo.setSendId(session.getUserId()); + msgInfo.setRecvId(friendId); + msgInfo.setSendTime(new Date()); + msgInfo.setType(MessageType.FRIEND_DND.code()); + msgInfo.setContent(isDnd.toString()); + IMPrivateMessage sendMessage = new IMPrivateMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setData(msgInfo); + sendMessage.setSendToSelf(true); + imClient.sendPrivateMessage(sendMessage); + } + } diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java index 6a8e23f..9302c61 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupMemberServiceImpl.java @@ -120,4 +120,12 @@ public class GroupMemberServiceImpl extends ServiceImpl wrapper = Wrappers.lambdaUpdate(); + wrapper.eq(GroupMember::getGroupId, groupId); + wrapper.eq(GroupMember::getUserId, userId); + wrapper.set(GroupMember::getIsDnd, isDnd); + this.update(wrapper); + } } diff --git a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java index 6564679..f5ab43d 100644 --- a/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java +++ b/im-platform/src/main/java/com/bx/implatform/service/impl/GroupServiceImpl.java @@ -12,6 +12,7 @@ import com.bx.imcommon.model.IMUserInfo; import com.bx.imcommon.util.CommaTextUtils; import com.bx.implatform.contant.Constant; import com.bx.implatform.contant.RedisKey; +import com.bx.implatform.dto.GroupDndDTO; import com.bx.implatform.dto.GroupInviteDTO; import com.bx.implatform.dto.GroupMemberRemoveDTO; import com.bx.implatform.entity.*; @@ -314,6 +315,15 @@ public class GroupServiceImpl extends ServiceImpl implements }).sorted((m1, m2) -> m2.getOnline().compareTo(m1.getOnline())).collect(Collectors.toList()); } + @Override + public void setDnd(GroupDndDTO dto) { + UserSession session = SessionContext.getSession(); + groupMemberService.setDnd(dto.getGroupId(), session.getUserId(), dto.getIsDnd()); + // 推送同步消息 + sendSyncDndMessage(dto.getGroupId(), dto.getIsDnd()); + } + + private void sendTipMessage(Long groupId, List recvIds, String content, Boolean sendToAll) { UserSession session = SessionContext.getSession(); // 消息入库 @@ -351,6 +361,7 @@ public class GroupServiceImpl extends ServiceImpl implements vo.setShowNickName(member.getShowNickName()); vo.setShowGroupName(StrUtil.blankToDefault(member.getRemarkGroupName(), group.getName())); vo.setQuit(member.getQuit()); + vo.setIsDnd(member.getIsDnd()); return vo; } @@ -386,4 +397,20 @@ public class GroupServiceImpl extends ServiceImpl implements sendMessage.setSendToSelf(false); imClient.sendGroupMessage(sendMessage); } + + private void sendSyncDndMessage(Long groupId, Boolean isDnd) { + UserSession session = SessionContext.getSession(); + GroupMessageVO msgInfo = new GroupMessageVO(); + msgInfo.setType(MessageType.GROUP_DND.code()); + msgInfo.setSendTime(new Date()); + msgInfo.setGroupId(groupId); + msgInfo.setSendId(session.getUserId()); + msgInfo.setContent(isDnd.toString()); + IMGroupMessage sendMessage = new IMGroupMessage<>(); + sendMessage.setSender(new IMUserInfo(session.getUserId(), session.getTerminal())); + sendMessage.setData(msgInfo); + sendMessage.setSendResult(false); + imClient.sendGroupMessage(sendMessage); + } + } diff --git a/im-platform/src/main/java/com/bx/implatform/vo/FriendVO.java b/im-platform/src/main/java/com/bx/implatform/vo/FriendVO.java index e36e6e3..cf65b05 100644 --- a/im-platform/src/main/java/com/bx/implatform/vo/FriendVO.java +++ b/im-platform/src/main/java/com/bx/implatform/vo/FriendVO.java @@ -20,6 +20,10 @@ public class FriendVO { @Schema(description = "好友头像") private String headImage; + @Schema(description = "是否开启免打扰") + private Boolean isDnd; + + @Schema(description = "是否已删除") private Boolean deleted; } diff --git a/im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java b/im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java index 75abfa8..1673ee2 100644 --- a/im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java +++ b/im-platform/src/main/java/com/bx/implatform/vo/GroupVO.java @@ -56,5 +56,8 @@ public class GroupVO { @Schema(description = "被封禁原因") private String reason; + @Schema(description = "是否开启免打扰") + private Boolean isDnd; + } diff --git a/im-uniapp/App.vue b/im-uniapp/App.vue index 9910879..e9264ca 100644 --- a/im-uniapp/App.vue +++ b/im-uniapp/App.vue @@ -146,6 +146,12 @@ export default { this.friendStore.removeFriend(friendId); return; } + // 对好友设置免打扰 + if (msg.type == enums.MESSAGE_TYPE.FRIEND_DND) { + this.friendStore.setDnd(friendId, JSON.parse(msg.content)); + this.chatStore.setDnd(chatInfo, JSON.parse(msg.content)); + return; + } // 消息插入 let friend = this.loadFriendInfo(friendId); this.insertPrivateMessage(friend, msg); @@ -183,14 +189,20 @@ export default { type: 'PRIVATE', targetId: friend.id, showName: friend.nickName, - headImage: friend.headImage + headImage: friend.headImage, + isDnd: friend.isDnd }; // 打开会话 this.chatStore.openChat(chatInfo); // 插入消息 this.chatStore.insertMessage(msg, chatInfo); // 播放提示音 - this.playAudioTip(); + this.chatStore.insertMessage(msg, chatInfo); + if (!friend.isDnd && !this.chatStore.isLoading() && + !msg.selfSend && msgType.isNormal(msg.type) && + msg.status != enums.MESSAGE_STATUS.READED) { + this.playAudioTip(); + } } @@ -240,6 +252,12 @@ export default { this.groupStore.removeGroup(msg.groupId); return; } + // 对群设置免打扰 + if (msg.type == enums.MESSAGE_TYPE.GROUP_DND) { + this.groupStore.setDnd(msg.groupId, JSON.parse(msg.content)); + this.chatStore.setDnd(chatInfo, JSON.parse(msg.content)); + return; + } // 插入消息 let group = this.loadGroupInfo(msg.groupId); this.insertGroupMessage(group, msg); @@ -291,14 +309,19 @@ export default { type: 'GROUP', targetId: group.id, showName: group.showGroupName, - headImage: group.headImageThumb + headImage: group.headImageThumb, + isDnd: group.isDnd }; // 打开会话 this.chatStore.openChat(chatInfo); // 插入消息 this.chatStore.insertMessage(msg, chatInfo); // 播放提示音 - this.playAudioTip(); + if (!group.isDnd && !this.chatStore.isLoading() && + !msg.selfSend && msgType.isNormal(msg.type) && + msg.status != enums.MESSAGE_STATUS.READED) { + this.playAudioTip(); + } } }, diff --git a/im-uniapp/common/enums.js b/im-uniapp/common/enums.js index fbdb7a3..3ddd09c 100644 --- a/im-uniapp/common/enums.js +++ b/im-uniapp/common/enums.js @@ -16,8 +16,10 @@ const MESSAGE_TYPE = { USER_BANNED: 50, FRIEND_NEW: 80, FRIEND_DEL: 81, + FRIEND_DND: 82, GROUP_NEW: 90, - GROUP_DEL: 91, + GROUP_DEL: 91, + GROUP_DND: 92, RTC_CALL_VOICE: 100, RTC_CALL_VIDEO: 101, RTC_ACCEPT: 102, diff --git a/im-uniapp/components/chat-item/chat-item.vue b/im-uniapp/components/chat-item/chat-item.vue index d0ed604..d7c8f7f 100644 --- a/im-uniapp/components/chat-item/chat-item.vue +++ b/im-uniapp/components/chat-item/chat-item.vue @@ -15,8 +15,10 @@ {{ atText }} {{ chat.sendNickName + ': ' }} - + + @@ -43,7 +45,7 @@ export default { methods: { showChatBox() { // 初始化期间进入会话会导致消息不刷新 - if(!getApp().$vm.isInit || this.chatStore.isLoading()){ + if (!getApp().$vm.isInit || this.chatStore.isLoading()) { uni.showToast({ title: "正在初始化页面,请稍后...", icon: 'none' @@ -153,8 +155,8 @@ export default { font-size: $im-font-size-smaller; color: $im-text-color-lighter; padding-top: 8rpx; - align-items: center; - + align-items: center; + .chat-at-text { color: $im-color-danger; } diff --git a/im-uniapp/im.scss b/im-uniapp/im.scss index e5aefe7..0a45626 100644 --- a/im-uniapp/im.scss +++ b/im-uniapp/im.scss @@ -102,7 +102,6 @@ button[size='mini'] { } } - .uni-radio-input svg{ border-color: white !important; background-color: $im-color-primary !important; @@ -171,6 +170,26 @@ button[size='mini'] { font-size: 10px !important; font-weight: bolder !important; } +.uni-switch-input-checked { + background-color: $im-color-primary-light-1 !important; + border-color: $im-color-primary-light-1 !important; +} +.uni-modal__title { + font-size: $im-font-size-larger !important; +} +.uni-modal__bd { + font-size: $im-font-size; +} +.uni-modal__ft { + font-size: $im-font-size; + line-height: 90rpx !important; + + .uni-modal__btn_primary { + color: $im-color-primary !important; + } +} + + .nav-bar { height: 100rpx; @@ -214,4 +233,19 @@ button[size='mini'] { width: 36rpx !important; height: 36rpx !important; vertical-align: bottom !important; -} \ No newline at end of file +} + +.none-pointer-events { + uni-image img { + // 阻止微信默认长按菜单 + pointer-events: none; + -webkit-pointer-events: none; + -ms-pointer-events: none; + -moz-pointer-events: none; + } +} + +p { + margin-block-start: 1em; + margin-block-end: 1em; +} diff --git a/im-uniapp/pages/chat/chat-box.vue b/im-uniapp/pages/chat/chat-box.vue index 8875d3d..c898442 100644 --- a/im-uniapp/pages/chat/chat-box.vue +++ b/im-uniapp/pages/chat/chat-box.vue @@ -133,8 +133,7 @@ export default { keyboardHeight: 290, // 键盘高度 windowHeight: 1000, // 窗口高度 initHeight: 1000, // h5初始高度 - atUserIds: [], - needScrollToBottom: false, // 需要滚动到底部 + atUserIds: [], showMinIdx: 0, // 下标小于showMinIdx的消息不显示,否则可能很卡 reqQueue: [], // 请求队列 isSending: false, // 是否正在发送请求 @@ -576,7 +575,6 @@ export default { }, 100) }, onScrollToTop() { - console.log("onScrollToTop") if (this.showMinIdx > 0) { // #ifndef H5 // 防止滚动条定格在顶部,不能一直往上滚 @@ -924,14 +922,17 @@ export default { }, watch: { messageSize: function(newSize, oldSize) { - // 接收到消息时滚动到底部 - if (newSize > oldSize) { - if (this.isInBottom) { - // 收到消息,则滚动至底部 - this.scrollToBottom(); - } else { - // 若滚动条不在底部,说明用户正在翻历史消息,此时滚动条不能动,同时增加新消息提示 - this.newMessageSize++; + // 接收到新消息 + if (newSize > oldSize && oldSize > 0) { + let lastMessage = this.chat.messages[newSize - 1]; + if (this.$msgType.isNormal(lastMessage.type)) { + if (this.isInBottom) { + // 收到消息,则滚动至底部 + this.scrollToBottom(); + } else { + // 若滚动条不在底部,说明用户正在翻历史消息,此时滚动条不能动,同时增加新消息提示 + this.newMessageSize++; + } } } }, @@ -963,13 +964,16 @@ export default { this.chatStore.activeChat(options.chatIdx); // 复位回执消息 this.isReceipt = false; + // 清空底部标志 + this.isInBottom = true; + this.newMessageSize = 0; // 监听键盘高度 this.listenKeyBoard(); // 计算聊天窗口高度 this.$nextTick(() => { this.windowHeight = uni.getSystemInfoSync().windowHeight; this.reCalChatMainHeight(); - this.scrollToBottom(); + // #ifdef H5 this.initHeight = window.innerHeight; // 兼容ios的h5:禁止页面滚动 @@ -982,13 +986,6 @@ export default { }, onUnload() { this.unListenKeyboard(); - }, - onShow() { - if (this.needScrollToBottom) { - // 页面滚到底部 - this.scrollToBottom(); - this.needScrollToBottom = false; - } } } diff --git a/im-uniapp/pages/chat/chat.vue b/im-uniapp/pages/chat/chat.vue index bd74f8a..c5bc1d3 100644 --- a/im-uniapp/pages/chat/chat.vue +++ b/im-uniapp/pages/chat/chat.vue @@ -6,7 +6,7 @@ 消息接收中... - + 正在初始化... @@ -17,7 +17,7 @@ placeholder="搜索"> - + 温馨提示:您现在还没有任何聊天消息,快跟您的好友发起聊天吧~ diff --git a/im-uniapp/pages/common/user-info.vue b/im-uniapp/pages/common/user-info.vue index 47e4ca7..bca79c2 100644 --- a/im-uniapp/pages/common/user-info.vue +++ b/im-uniapp/pages/common/user-info.vue @@ -14,17 +14,23 @@ 用户名: - {{ userInfo.userName }} + {{ userInfo.userName }} 签名: - {{ userInfo.signature }} + {{ userInfo.signature }} + + + + + + @@ -57,6 +63,9 @@ export default { showName: this.userInfo.nickName, headImage: this.userInfo.headImage, }; + if (this.isFriend) { + chat.isDnd = this.friendInfo.isDnd; + } this.chatStore.openChat(chat); let chatIdx = this.chatStore.findChatIdx(chat); uni.navigateTo({ @@ -103,6 +112,41 @@ export default { } }) }, + onCleanMessage() { + uni.showModal({ + title: '清空聊天记录', + content: `确认删除与'${this.userInfo.nickName}'的聊天记录吗?`, + confirmText: '确认', + success: (res) => { + if (res.cancel) + return; + this.chatStore.cleanMessage(this.chatIdx); + uni.showToast({ + title: `您清空了'${this.userInfo.nickName}'的聊天记录`, + icon: 'none' + }) + } + }) + }, + onDndChange(e) { + let isDnd = e.detail.value; + let friendId = this.userInfo.id; + let formData = { + friendId: friendId, + isDnd: isDnd + } + this.$http({ + url: '/friend/dnd', + method: 'PUT', + data: formData + }).then(() => { + this.friendStore.setDnd(friendId, isDnd) + let chat = this.chatStore.findChatByFriend(friendId) + if (chat) { + this.chatStore.setDnd(chat, isDnd) + } + }) + }, updateFriendInfo() { if (this.isFriend) { // store的数据不能直接修改,深拷贝一份store的数据 @@ -134,6 +178,13 @@ export default { }, friendInfo() { return this.friendStore.findFriend(this.userInfo.id); + }, + chatIdx() { + let chat = this.chatStore.findChatByFriend(this.userInfo.id); + if (chat) { + return this.chatStore.findChatIdx(chat); + } + return -1; } }, onLoad(options) { diff --git a/im-uniapp/pages/group/group-info.vue b/im-uniapp/pages/group/group-info.vue index 31d7c1a..bf1f4e9 100644 --- a/im-uniapp/pages/group/group-info.vue +++ b/im-uniapp/pages/group/group-info.vue @@ -52,6 +52,12 @@ 修改群聊资料 > + + + + + + @@ -117,6 +123,7 @@ export default { targetId: this.group.id, showName: this.group.showGroupName, headImage: this.group.headImage, + isDnd: this.group.isDnd }; this.chatStore.openChat(chat); let chatIdx = this.chatStore.findChatIdx(chat); @@ -178,6 +185,42 @@ export default { } }); }, + onDndChange(e) { + let isDnd = e.detail.value; + let groupId = this.group.id; + let formData = { + groupId: groupId, + isDnd: isDnd + } + this.$http({ + url: '/group/dnd', + method: 'PUT', + data: formData + }).then(() => { + this.groupStore.setDnd(groupId, isDnd); + let chat = this.chatStore.findChatByGroup(groupId); + if (chat) { + this.chatStore.setDnd(chat, isDnd) + } + }) + }, + onCleanMessage() { + uni.showModal({ + title: '清空聊天记录', + content: `确认删除群聊'${this.group.name}'的聊天记录吗?`, + confirmText: '确认', + success: (res) => { + if (res.cancel) { + return; + } + this.chatStore.cleanMessage(this.chatIdx); + uni.showToast({ + title: `您清空了'${this.group.name}'的聊天记录`, + icon: 'none' + }) + } + }) + }, loadGroupInfo() { this.$http({ url: `/group/find/${this.groupId}`, @@ -210,6 +253,13 @@ export default { }, showMaxIdx() { return this.isOwner ? 8 : 9; + }, + chatIdx() { + let chat = this.chatStore.findChatByGroup(this.groupId); + if (chat) { + return this.chatStore.findChatIdx(chat); + } + return -1; } }, onLoad(options) { diff --git a/im-uniapp/static/icon/iconfont.css b/im-uniapp/static/icon/iconfont.css index 8cda2a4..1e24c60 100644 --- a/im-uniapp/static/icon/iconfont.css +++ b/im-uniapp/static/icon/iconfont.css @@ -1,6 +1,6 @@ @font-face { font-family: "iconfont"; /* Project id 4272106 */ - src: url('iconfont.ttf?t=1746119818070') format('truetype'); + src: url('iconfont.ttf?t=1750317465456') format('truetype'); } .iconfont { @@ -11,6 +11,34 @@ -moz-osx-font-smoothing: grayscale; } +.icon-dnd:before { + content: "\e693"; +} + +.icon-privacy-protocol:before { + content: "\e761"; +} + +.icon-create-group-2:before { + content: "\e616"; +} + +.icon-create-group:before { + content: "\e650"; +} + +.icon-qrcode:before { + content: "\e642"; +} + +.icon-add-friend:before { + content: "\e64f"; +} + +.icon-scan:before { + content: "\e8b5"; +} + .icon-remove:before { content: "\e603"; } @@ -51,10 +79,6 @@ content: "\ec44"; } -.icon-privacy-protocol:before { - content: "\e70a"; -} - .icon-un-register:before { content: "\e656"; } diff --git a/im-uniapp/static/icon/iconfont.ttf b/im-uniapp/static/icon/iconfont.ttf index c2fee58..a133860 100644 Binary files a/im-uniapp/static/icon/iconfont.ttf and b/im-uniapp/static/icon/iconfont.ttf differ diff --git a/im-uniapp/store/chatStore.js b/im-uniapp/store/chatStore.js index 27e91c8..efbb562 100644 --- a/im-uniapp/store/chatStore.js +++ b/im-uniapp/store/chatStore.js @@ -1,5 +1,7 @@ import { defineStore } from 'pinia'; import { MESSAGE_TYPE, MESSAGE_STATUS } from '@/common/enums.js'; +import useFriendStore from './friendStore.js'; +import useGroupStore from './groupStore.js'; import useUserStore from './userStore'; let cacheChats = []; @@ -56,6 +58,7 @@ export default defineStore('chatStore', { type: chatInfo.type, showName: chatInfo.showName, headImage: chatInfo.headImage, + isDnd: chatInfo.isDnd, lastContent: "", lastSendTime: new Date().getTime(), unreadCount: 0, @@ -105,6 +108,17 @@ export default defineStore('chatStore', { this.saveToStorage(); } }, + cleanMessage(idx) { + let chat = this.curChats[idx]; + chat.lastContent = ''; + chat.hotMinIdx = 0; + chat.unreadCount = 0; + chat.atMe = false; + chat.atAll = false; + chat.stored = false + chat.messages = []; + this.saveToStorage(true); + }, removeChat(idx) { let chats = this.curChats; chats[idx].delete = true; @@ -181,7 +195,7 @@ export default defineStore('chatStore', { chat.lastSendTime = msgInfo.sendTime; chat.sendNickName = msgInfo.sendNickName; // 未读加1 - if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED && + if (!chat.isDnd && !msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED && msgInfo.status != MESSAGE_STATUS.RECALL && msgInfo.type != MESSAGE_TYPE.TIP_TEXT) { chat.unreadCount++; } @@ -336,8 +350,34 @@ export default defineStore('chatStore', { this.refreshChats() } }, + setDnd(chatInfo, isDnd) { + let chat = this.findChat(chatInfo); + if (chat) { + chat.isDnd = isDnd; + chat.unreadCount = 0; + } + }, refreshChats() { if (!cacheChats) return; + // 更新会话免打扰状态 + const friendStore = useFriendStore(); + const groupStore = useGroupStore(); + cacheChats.forEach(chat => { + if (chat.type == 'PRIVATE') { + let friend = friendStore.findFriend(chat.targetId); + if (friend) { + chat.isDnd = friend.isDnd + } + } else if (chat.type == 'GROUP') { + let group = groupStore.findGroup(chat.targetId); + if (group) { + chat.isDnd = group.isDnd + } + } + if (chat.isDnd) { + chat.unreadCount = 0; + } + }) // 排序 cacheChats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime); // #ifndef APP-PLUS @@ -345,8 +385,8 @@ export default defineStore('chatStore', { * 由于h5和小程序的stroge只有5m,大约只能存储2w条消息, * 所以这里每个会话只保留1000条消息,防止溢出 */ - cacheChats.forEach(chat =>{ - if(chat.messages.length > 1000){ + cacheChats.forEach(chat => { + if (chat.messages.length > 1000) { let idx = chat.messages.length - 1000; chat.messages = chat.messages.slice(idx); } diff --git a/im-uniapp/store/friendStore.js b/im-uniapp/store/friendStore.js index c337efd..033f856 100644 --- a/im-uniapp/store/friendStore.js +++ b/im-uniapp/store/friendStore.js @@ -67,6 +67,10 @@ export default defineStore('friendStore', { this.refreshOnlineStatus(); }, 30000) }, + setDnd(id, isDnd) { + let friend = this.findFriend(id); + friend.isDnd = isDnd; + }, clear() { clearTimeout(this.timer); this.friends = []; diff --git a/im-uniapp/store/groupStore.js b/im-uniapp/store/groupStore.js index 2c4cc66..739361b 100644 --- a/im-uniapp/store/groupStore.js +++ b/im-uniapp/store/groupStore.js @@ -25,6 +25,10 @@ export default defineStore('groupStore', { let g = this.findGroup(group.id); Object.assign(g, group); }, + setDnd(id, isDnd) { + let group = this.findGroup(id); + group.isDnd = isDnd; + }, clear() { this.groups = []; }, diff --git a/im-web/src/api/enums.js b/im-web/src/api/enums.js index 9bd298d..ae9133c 100644 --- a/im-web/src/api/enums.js +++ b/im-web/src/api/enums.js @@ -15,8 +15,10 @@ const MESSAGE_TYPE = { USER_BANNED: 50, FRIEND_NEW: 80, FRIEND_DEL: 81, + FRIEND_DND: 82, GROUP_NEW: 90, GROUP_DEL: 91, + GROUP_DND: 92, RTC_CALL_VOICE: 100, RTC_CALL_VIDEO: 101, RTC_ACCEPT: 102, diff --git a/im-web/src/assets/iconfont/iconfont.css b/im-web/src/assets/iconfont/iconfont.css index f435d95..390d4d4 100644 --- a/im-web/src/assets/iconfont/iconfont.css +++ b/im-web/src/assets/iconfont/iconfont.css @@ -1,6 +1,6 @@ @font-face { font-family: "iconfont"; /* Project id 3791506 */ - src: url('iconfont.ttf?t=1745933248800') format('truetype'); + src: url('iconfont.ttf?t=1750245745055') format('truetype'); } .iconfont { @@ -11,6 +11,30 @@ -moz-osx-font-smoothing: grayscale; } +.icon-dnd:before { + content: "\e691"; +} + +.icon-screenshot:before { + content: "\e61c"; +} + +.icon-close:before { + content: "\e609"; +} + +.icon-minimize:before { + content: "\e650"; +} + +.icon-maximize:before { + content: "\e651"; +} + +.icon-unmaximize:before { + content: "\e611"; +} + .icon-man:before { content: "\e615"; } diff --git a/im-web/src/assets/iconfont/iconfont.ttf b/im-web/src/assets/iconfont/iconfont.ttf index e6c7a35..1e570e9 100644 Binary files a/im-web/src/assets/iconfont/iconfont.ttf and b/im-web/src/assets/iconfont/iconfont.ttf differ diff --git a/im-web/src/components/chat/ChatItem.vue b/im-web/src/components/chat/ChatItem.vue index f67b4ef..8ad7a5b 100644 --- a/im-web/src/components/chat/ChatItem.vue +++ b/im-web/src/components/chat/ChatItem.vue @@ -9,14 +9,15 @@
{{ chat.showName }}
- +
{{ showTime }}
{{ atText }}
{{ chat.sendNickName + ': ' }}
-
+
+
@@ -36,15 +37,6 @@ export default { }, data() { return { - menuItems: [{ - key: 'TOP', - name: '置顶', - icon: 'el-icon-top' - }, { - key: 'DELETE', - name: '删除', - icon: 'el-icon-delete' - }] } }, props: { @@ -86,6 +78,30 @@ export default { return "[@全体成员]" } return ""; + }, + menuItems() { + let items = []; + items.push({ + key: 'TOP', + name: '置顶' + }); + if (this.chat.isDnd) { + items.push({ + key: 'DND', + name: '新消息提醒' + }) + } else { + items.push({ + key: 'DND', + name: '消息免打扰' + }) + } + items.push({ + key: 'DELETE', + name: '删除聊天', + color: '#F56C6C' + }) + return items; } } } @@ -185,7 +201,7 @@ export default { font-size: var(--im-font-size-small); color: var(--im-text-color-light); } - + .chat-content-text { flex: 1; white-space: nowrap; @@ -195,6 +211,9 @@ export default { color: var(--im-text-color-light); } + .icon { + color: var(--im-text-color-light); + } } } } diff --git a/im-web/src/components/common/UserInfo.vue b/im-web/src/components/common/UserInfo.vue index 6a33988..913f714 100644 --- a/im-web/src/components/common/UserInfo.vue +++ b/im-web/src/components/common/UserInfo.vue @@ -65,8 +65,11 @@ export default { type: 'PRIVATE', targetId: user.id, showName: user.nickName, - headImage: user.headImage, + headImage: user.headImage }; + if (this.isFriend) { + chat.isDnd = this.friendInfo.isDnd; + } this.chatStore.openChat(chat); this.chatStore.setActiveChat(0); if (this.$route.path != "/home/chat") { @@ -102,6 +105,9 @@ export default { computed: { isFriend() { return this.friendStore.isFriend(this.user.id); + }, + friendInfo() { + return this.friendStore.findFriend(this.user.id); } } } diff --git a/im-web/src/store/chatStore.js b/im-web/src/store/chatStore.js index 712a162..3922ee4 100644 --- a/im-web/src/store/chatStore.js +++ b/im-web/src/store/chatStore.js @@ -1,5 +1,7 @@ import { defineStore } from 'pinia'; import { MESSAGE_TYPE, MESSAGE_STATUS } from "../api/enums.js" +import useFriendStore from './friendStore.js'; +import useGroupStore from './groupStore.js'; import useUserStore from './userStore.js'; import localForage from 'localforage'; @@ -66,6 +68,7 @@ export default defineStore('chatStore', { type: chatInfo.type, showName: chatInfo.showName, headImage: chatInfo.headImage, + isDnd: chatInfo.isDnd, lastContent: "", lastSendTime: new Date().getTime(), unreadCount: 0, @@ -193,7 +196,7 @@ export default defineStore('chatStore', { chat.lastSendTime = msgInfo.sendTime; chat.sendNickName = msgInfo.sendNickName; // 未读加1 - if (!msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED && + if (!chat.isDnd && !msgInfo.selfSend && msgInfo.status != MESSAGE_STATUS.READED && msgInfo.status != MESSAGE_STATUS.RECALL && msgInfo.type != MESSAGE_TYPE.TIP_TEXT) { chat.unreadCount++; } @@ -343,8 +346,34 @@ export default defineStore('chatStore', { this.refreshChats(); } }, + setDnd(chatInfo, isDnd) { + let chat = this.findChat(chatInfo); + if (chat) { + chat.isDnd = isDnd; + chat.unreadCount = 0; + } + }, refreshChats() { if (!cacheChats) return; + // 刷新免打扰状态 + const friendStore = useFriendStore(); + const groupStore = useGroupStore(); + cacheChats.forEach(chat => { + if (chat.type == 'PRIVATE') { + let friend = friendStore.findFriend(chat.targetId); + if (friend) { + chat.isDnd = friend.isDnd + } + } else if (chat.type == 'GROUP') { + let group = groupStore.findGroup(chat.targetId); + if (group) { + chat.isDnd = group.isDnd + } + } + if (chat.isDnd) { + chat.unreadCount = 0; + } + }) // 排序 cacheChats.sort((chat1, chat2) => chat2.lastSendTime - chat1.lastSendTime); // 记录热数据索引位置 diff --git a/im-web/src/store/friendStore.js b/im-web/src/store/friendStore.js index 8051571..ff95289 100644 --- a/im-web/src/store/friendStore.js +++ b/im-web/src/store/friendStore.js @@ -42,6 +42,10 @@ export default defineStore('friendStore', { } friend.online = friend.onlineWeb || friend.onlineApp; }, + setDnd(id, isDnd) { + let friend = this.findFriend(id); + friend.isDnd = isDnd; + }, clear() { this.timer && clearTimeout(this.timer); this.friends = []; diff --git a/im-web/src/store/groupStore.js b/im-web/src/store/groupStore.js index c493fea..e16a0e7 100644 --- a/im-web/src/store/groupStore.js +++ b/im-web/src/store/groupStore.js @@ -35,6 +35,10 @@ export default defineStore('groupStore', { group.topMessage = topMessage; } }, + setDnd(id, isDnd) { + let group = this.findGroup(id); + group.isDnd = isDnd; + }, clear() { this.groups = []; }, diff --git a/im-web/src/view/Chat.vue b/im-web/src/view/Chat.vue index f45f706..645b1e4 100644 --- a/im-web/src/view/Chat.vue +++ b/im-web/src/view/Chat.vue @@ -13,7 +13,7 @@
+ @dnd="onDnd(chat)" :active="chat === chatStore.activeChat">
@@ -51,6 +51,41 @@ export default { onTop(chatIdx) { this.chatStore.moveTop(chatIdx); }, + onDnd(chat) { + if (chat.type == 'PRIVATE') { + this.setFriendDnd(chat, chat.targetId, !chat.isDnd) + } else { + this.setGroupDnd(chat, chat.targetId, !chat.isDnd) + } + }, + setFriendDnd(chat, friendId, isDnd) { + let formData = { + friendId: friendId, + isDnd: isDnd + } + this.$http({ + url: '/friend/dnd', + method: 'put', + data: formData + }).then(() => { + this.friendStore.setDnd(friendId, isDnd) + this.chatStore.setDnd(chat, isDnd) + }) + }, + setGroupDnd(chat, groupId, isDnd) { + let formData = { + groupId: groupId, + isDnd: isDnd + } + this.$http({ + url: '/group/dnd', + method: 'put', + data: formData + }).then(() => { + this.groupStore.setDnd(groupId, isDnd) + this.chatStore.setDnd(chat, isDnd) + }) + } }, computed: { loading() { diff --git a/im-web/src/view/Friend.vue b/im-web/src/view/Friend.vue index 7ad77ab..c694891 100644 --- a/im-web/src/view/Friend.vue +++ b/im-web/src/view/Friend.vue @@ -44,7 +44,7 @@
发消息 + @click="onSendMessage(activeFriend)">发消息 加为好友 { @@ -342,15 +355,17 @@ export default { type: 'GROUP', targetId: group.id, showName: group.showGroupName, - headImage: group.headImageThumb + headImage: group.headImageThumb, + isDnd: group.isDnd }; // 打开会话 this.chatStore.openChat(chatInfo); // 插入消息 this.chatStore.insertMessage(msg, chatInfo); // 播放提示音 - if (!msg.selfSend && msg.type <= this.$enums.MESSAGE_TYPE.VIDEO && - msg.status != this.$enums.MESSAGE_STATUS.READED) { + if (!group.isDnd && !this.chatStore.isLoading() && + !msg.selfSend && this.$msgType.isNormal(msg.type) + && msg.status != this.$enums.MESSAGE_STATUS.READED) { this.playAudioTip(); } },