前端:
增加会话缓存 后端: 增加触发消息创建事件 增加消息创建事件触发后的处理函数
This commit is contained in:
@@ -30,58 +30,26 @@ namespace IM_API.Application.EventHandlers
|
|||||||
_context = imContext;
|
_context = imContext;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* 此方法有并发问题,当双方同时第一次发送消息时,
|
|
||||||
* 会出现同时创建的情况,其中一方会报错,
|
|
||||||
* 导致也未走到更新逻辑,会话丢失
|
|
||||||
*/
|
|
||||||
public async Task Handle(MessageCreatedEvent @event)
|
public async Task Handle(MessageCreatedEvent @event)
|
||||||
{
|
{
|
||||||
//此处仅处理私聊会话创建
|
if(@event.ChatType == ChatType.PRIVATE)
|
||||||
if (@event.ChatType == ChatType.GROUP)
|
|
||||||
{
|
{
|
||||||
return;
|
Conversation? userAConversation = await _context.Conversations.FirstOrDefaultAsync(
|
||||||
}
|
x => x.UserId == @event.MsgSenderId && x.TargetId == @event.MsgRecipientId
|
||||||
var conversation = await _context.Conversations.FirstOrDefaultAsync(
|
);
|
||||||
x => x.UserId == @event.MsgSenderId && x.TargetId == @event.MsgRecipientId
|
Conversation? userBConversation = await _context.Conversations.FirstOrDefaultAsync(
|
||||||
);
|
x => x.UserId == @event.MsgRecipientId && x.TargetId == @event.MsgSenderId
|
||||||
//如果首次发消息则创建双方会话
|
);
|
||||||
if (conversation is null)
|
if(userAConversation is null || userBConversation is null)
|
||||||
{
|
|
||||||
Conversation senderCon = _mapper.Map<Conversation>(@event);
|
|
||||||
Conversation ReceptCon = _mapper.Map<Conversation>(@event);
|
|
||||||
ReceptCon.UserId = @event.MsgRecipientId;
|
|
||||||
ReceptCon.TargetId = @event.MsgSenderId;
|
|
||||||
ReceptCon.UnreadCount += 1;
|
|
||||||
ReceptCon.LastReadMessageId = null;
|
|
||||||
_context.Conversations.AddRange(senderCon,ReceptCon);
|
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Conversation senderCon = conversation;
|
|
||||||
Conversation? ReceptCon = await _context.Conversations.FirstOrDefaultAsync(
|
|
||||||
x => x.UserId == @event.MsgRecipientId && x.TargetId == @event.MsgSenderId);
|
|
||||||
if (ReceptCon is null)
|
|
||||||
{
|
{
|
||||||
_logger.LogError("ConversationEventHandlerError:接收者会话对象缺失!Event:{Event}", JsonConvert.SerializeObject(@event));
|
_logger.LogError("消息事件更新会话信息失败:{@event}",@event);
|
||||||
throw new BaseException(CodeDefine.SYSTEM_ERROR);
|
|
||||||
}
|
}
|
||||||
|
userAConversation.LastMessage = @event.MessageContent;
|
||||||
//更新发送者conversation
|
userAConversation.LastReadMessageId = @event.MessageId;
|
||||||
senderCon.UnreadCount = 0;
|
userBConversation.LastMessage = @event.MessageContent;
|
||||||
senderCon.LastReadMessageId = @event.MessageId;
|
userBConversation.UnreadCount += 1;
|
||||||
senderCon.LastMessage = @event.MessageContent;
|
_context.UpdateRange(userAConversation,userBConversation);
|
||||||
senderCon.LastMessageTime = DateTime.Now;
|
|
||||||
//更新接收者conversation
|
|
||||||
ReceptCon.UnreadCount += 1;
|
|
||||||
ReceptCon.LastMessage = @event.MessageContent;
|
|
||||||
senderCon.LastMessageTime = DateTime.Now;
|
|
||||||
_context.Conversations.UpdateRange(senderCon, ReceptCon);
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using IM_API.Application.Interfaces;
|
using AutoMapper;
|
||||||
|
using IM_API.Application.Interfaces;
|
||||||
using IM_API.Domain.Events;
|
using IM_API.Domain.Events;
|
||||||
|
using IM_API.Dtos;
|
||||||
using IM_API.Hubs;
|
using IM_API.Hubs;
|
||||||
|
using IM_API.Models;
|
||||||
using IM_API.Tools;
|
using IM_API.Tools;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
@@ -9,15 +12,20 @@ namespace IM_API.Application.EventHandlers
|
|||||||
public class SignalREventHandler : IEventHandler<MessageCreatedEvent>
|
public class SignalREventHandler : IEventHandler<MessageCreatedEvent>
|
||||||
{
|
{
|
||||||
private readonly IHubContext<ChatHub> _hub;
|
private readonly IHubContext<ChatHub> _hub;
|
||||||
public SignalREventHandler(IHubContext<ChatHub> hub)
|
private readonly IMapper _mapper;
|
||||||
|
public SignalREventHandler(IHubContext<ChatHub> hub, IMapper mapper)
|
||||||
{
|
{
|
||||||
_hub = hub;
|
_hub = hub;
|
||||||
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Handle(MessageCreatedEvent @event)
|
public async Task Handle(MessageCreatedEvent @event)
|
||||||
{
|
{
|
||||||
var streamKey = @event.StreamKey;
|
if(@event.ChatType == Models.ChatType.PRIVATE)
|
||||||
await _hub.Clients.Group(streamKey).SendAsync(SignalRMethodDefine.ReceiveMessage, @event);
|
{
|
||||||
|
MessageBaseDto messageBaseDto = _mapper.Map<MessageBaseDto>(_mapper.Map<Message>(@event));
|
||||||
|
await _hub.Clients.Users(@event.MsgRecipientId.ToString()).SendAsync("ReceiveMessage", messageBaseDto);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
using IM_API.Dtos;
|
using AutoMapper;
|
||||||
|
using IM_API.Application.Interfaces;
|
||||||
|
using IM_API.Domain.Events;
|
||||||
|
using IM_API.Dtos;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
using IM_API.Models;
|
using IM_API.Models;
|
||||||
using IM_API.Tools;
|
using IM_API.Tools;
|
||||||
@@ -11,10 +14,14 @@ namespace IM_API.Hubs
|
|||||||
{
|
{
|
||||||
private IMessageSevice _messageService;
|
private IMessageSevice _messageService;
|
||||||
private readonly IConversationService _conversationService;
|
private readonly IConversationService _conversationService;
|
||||||
public ChatHub(IMessageSevice messageService, IConversationService conversationService)
|
private readonly IEventBus _eventBus;
|
||||||
|
private readonly IMapper _mapper;
|
||||||
|
public ChatHub(IMessageSevice messageService, IConversationService conversationService, IEventBus eventBus, IMapper mapper)
|
||||||
{
|
{
|
||||||
_messageService = messageService;
|
_messageService = messageService;
|
||||||
_conversationService = conversationService;
|
_conversationService = conversationService;
|
||||||
|
_eventBus = eventBus;
|
||||||
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async override Task OnConnectedAsync()
|
public async override Task OnConnectedAsync()
|
||||||
@@ -51,8 +58,6 @@ namespace IM_API.Hubs
|
|||||||
{
|
{
|
||||||
msgInfo = await _messageService.SendGroupMessageAsync(int.Parse(userIdStr), dto.ReceiverId, dto);
|
msgInfo = await _messageService.SendGroupMessageAsync(int.Parse(userIdStr), dto.ReceiverId, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Clients.Users(dto.ReceiverId.ToString()).SendAsync("ReceiveMessage", msgInfo);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
|
using IM_API.Application.Interfaces;
|
||||||
|
using IM_API.Domain.Events;
|
||||||
using IM_API.Dtos;
|
using IM_API.Dtos;
|
||||||
using IM_API.Exceptions;
|
using IM_API.Exceptions;
|
||||||
using IM_API.Interface.Services;
|
using IM_API.Interface.Services;
|
||||||
@@ -13,11 +15,13 @@ namespace IM_API.Services
|
|||||||
private readonly ImContext _context;
|
private readonly ImContext _context;
|
||||||
private readonly ILogger<MessageService> _logger;
|
private readonly ILogger<MessageService> _logger;
|
||||||
private readonly IMapper _mapper;
|
private readonly IMapper _mapper;
|
||||||
public MessageService(ImContext context, ILogger<MessageService> logger, IMapper mapper)
|
private readonly IEventBus _eventBus;
|
||||||
|
public MessageService(ImContext context, ILogger<MessageService> logger, IMapper mapper, IEventBus eventBus)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
|
_eventBus = eventBus;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<List<MessageBaseDto>> GetMessagesAsync(int userId, int conversationId, int? msgId, int? pageSize, bool desc)
|
public async Task<List<MessageBaseDto>> GetMessagesAsync(int userId, int conversationId, int? msgId, int? pageSize, bool desc)
|
||||||
@@ -92,6 +96,7 @@ namespace IM_API.Services
|
|||||||
message.StreamKey = StreamKeyBuilder.Group(groupId);
|
message.StreamKey = StreamKeyBuilder.Group(groupId);
|
||||||
_context.Messages.Add(message);
|
_context.Messages.Add(message);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
await _eventBus.PublishAsync(_mapper.Map<MessageCreatedEvent>(message));
|
||||||
return _mapper.Map<MessageBaseDto>(message);
|
return _mapper.Map<MessageBaseDto>(message);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -107,6 +112,7 @@ namespace IM_API.Services
|
|||||||
message.StreamKey = StreamKeyBuilder.Private(dto.SenderId, dto.ReceiverId);
|
message.StreamKey = StreamKeyBuilder.Private(dto.SenderId, dto.ReceiverId);
|
||||||
_context.Messages.Add(message);
|
_context.Messages.Add(message);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
await _eventBus.PublishAsync(_mapper.Map<MessageCreatedEvent>(message));
|
||||||
return _mapper.Map<MessageBaseDto>(message);
|
return _mapper.Map<MessageBaseDto>(message);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export const useChatStore = defineStore('chat', {
|
|||||||
this.messages = [];
|
this.messages = [];
|
||||||
//先从浏览器缓存加载一部分消息列表
|
//先从浏览器缓存加载一部分消息列表
|
||||||
const localHistory = await messagesDb.getPageMessages(sessionId, new Date().toISOString(), this.pageSize);
|
const localHistory = await messagesDb.getPageMessages(sessionId, new Date().toISOString(), this.pageSize);
|
||||||
console.log(localHistory)
|
|
||||||
if (localHistory.length > 0) {
|
if (localHistory.length > 0) {
|
||||||
this.messages = localHistory;
|
this.messages = localHistory;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
import { messageService } from "@/services/message";
|
||||||
|
import { conversationDb } from "@/utils/db/conversationDB";
|
||||||
|
import { useMessage } from "@/components/messages/useAlert";
|
||||||
|
|
||||||
|
const message = useMessage();
|
||||||
|
|
||||||
|
export const useConversationStore = defineStore('conversation', {
|
||||||
|
state: () => ({
|
||||||
|
conversations: []
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
async addConversation(conversation) {
|
||||||
|
await conversationDb.save(conversation);
|
||||||
|
this.conversations.unshift(conversation)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 加载当前会话消息列表
|
||||||
|
*/
|
||||||
|
async loadUserConversations() {
|
||||||
|
if (this.conversations.length == 0) {
|
||||||
|
try {
|
||||||
|
const covnersationsCache = await conversationDb.getAll();
|
||||||
|
if (covnersationsCache && covnersationsCache.length > 0) {
|
||||||
|
covnersationsCache.sort((a, b) => {
|
||||||
|
return new Date(a.dateTime) - new Date(b.dateTime);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
message.error('读取本地会话缓存失败...');
|
||||||
|
console.log('读取本地会话缓存失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await this.fetchConversationsFromServier()
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 从服务器加载新消息
|
||||||
|
* @param {*} sessionId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async fetchConversationsFromServier() {
|
||||||
|
const newConversations = (await messageService.getConversations()).data;
|
||||||
|
if (newConversations.length > 0) {
|
||||||
|
// 1. 将当前的本地数据转为 Map,方便通过 ID 快速查找 (O(1) 复杂度)
|
||||||
|
const localMap = new Map(this.conversations.map(item => [item.id, item]));
|
||||||
|
newConversations.forEach(item => {
|
||||||
|
const existingItem = localMap.get(item.id);
|
||||||
|
if (existingItem) {
|
||||||
|
// --- 局部更新 ---
|
||||||
|
// 使用 Object.assign 将新数据合并到旧对象上,保持响应式引用
|
||||||
|
Object.assign(existingItem, item);
|
||||||
|
} else {
|
||||||
|
// --- 插入新会话 ---
|
||||||
|
this.conversations.unshift(item);
|
||||||
|
}
|
||||||
|
// 同步到本地数据库
|
||||||
|
conversationDb.save(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { openDB } from "idb";
|
||||||
|
|
||||||
|
const DBNAME = 'IM_DB';
|
||||||
|
const STORE_NAME = 'messages';
|
||||||
|
const CONVERSARION_STORE_NAME = 'conversations';
|
||||||
|
|
||||||
|
export const dbPromise = openDB(DBNAME, 2, {
|
||||||
|
upgrade(db) {
|
||||||
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
|
const store = db.createObjectStore(STORE_NAME, { keyPath: 'msgId' });
|
||||||
|
store.createIndex('by-sessionId', 'sessionId');
|
||||||
|
store.createIndex('by-time', 'timeStamp');
|
||||||
|
}
|
||||||
|
if (!db.objectStoreNames.contains(CONVERSARION_STORE_NAME)) {
|
||||||
|
const store = db.createObjectStore(CONVERSARION_STORE_NAME, { keyPath: 'id' });
|
||||||
|
store.createIndex('by-id', 'id');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { dbPromise } from "./baseDb";
|
||||||
|
|
||||||
|
const STORE_NAME = 'conversations';
|
||||||
|
|
||||||
|
export const conversationDb = {
|
||||||
|
async save(conversation) {
|
||||||
|
(await dbPromise).put(STORE_NAME, conversation);
|
||||||
|
},
|
||||||
|
async getById(id) {
|
||||||
|
return (await dbPromise).getFromIndex(STORE_NAME, 'by-id', id);
|
||||||
|
},
|
||||||
|
async getAll() {
|
||||||
|
return (await dbPromise).getAll(STORE_NAME);
|
||||||
|
},
|
||||||
|
async clearAll() {
|
||||||
|
(await dbPromise).clear(STORE_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,7 @@
|
|||||||
import { openDB } from "idb";
|
import { dbPromise } from "./baseDb";
|
||||||
|
|
||||||
const DBNAME = 'IM_DB';
|
|
||||||
const STORE_NAME = 'messages';
|
const STORE_NAME = 'messages';
|
||||||
|
|
||||||
export const dbPromise = openDB(DBNAME, 1, {
|
|
||||||
upgrade(db) {
|
|
||||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
||||||
const store = db.createObjectStore(STORE_NAME, { keyPath: 'msgId' });
|
|
||||||
store.createIndex('by-sessionId', 'sessionId');
|
|
||||||
store.createIndex('by-time', 'timeStamp');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export const messagesDb = {
|
export const messagesDb = {
|
||||||
async save(msg) {
|
async save(msg) {
|
||||||
return (await dbPromise).put(STORE_NAME, msg);
|
return (await dbPromise).put(STORE_NAME, msg);
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { formatDate } from '@/utils/formatDate';
|
|||||||
import { useChatStore } from '@/stores/chat';
|
import { useChatStore } from '@/stores/chat';
|
||||||
import { generateSessionId } from '@/utils/sessionIdTools';
|
import { generateSessionId } from '@/utils/sessionIdTools';
|
||||||
import { useSignalRStore } from '@/stores/signalr';
|
import { useSignalRStore } from '@/stores/signalr';
|
||||||
|
import { useConversationStore } from '@/stores/conversation';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id:{
|
id:{
|
||||||
@@ -60,6 +61,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
const signalRStore = useSignalRStore();
|
const signalRStore = useSignalRStore();
|
||||||
|
const conversationStore = useConversationStore();
|
||||||
|
|
||||||
const input = ref(''); // 输入框内容
|
const input = ref(''); // 输入框内容
|
||||||
const historyRef = ref(null); // 绑定 DOM 用于滚动
|
const historyRef = ref(null); // 绑定 DOM 用于滚动
|
||||||
@@ -118,20 +120,20 @@ function toggleEmoji() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadConversation(conversationId) {
|
async function loadConversation(conversationId) {
|
||||||
|
/*
|
||||||
const res = await messageService.getConversationById(conversationId);
|
const res = await messageService.getConversationById(conversationId);
|
||||||
conversationInfo.value = res.data;
|
conversationInfo.value = res.data;
|
||||||
console.log(res)
|
*/
|
||||||
}
|
if(conversationStore.conversations.length == 0){
|
||||||
|
await conversationStore.loadUserConversations();
|
||||||
async function loadMessages(conversationId, msgId = null, pageSize = null) {
|
}
|
||||||
const res = await messageService.getMessages(conversationId);
|
conversationInfo.value = conversationStore.conversations.find(x => x.id == Number(conversationId));
|
||||||
messages.value = res.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化时滚动到底部
|
// 初始化时滚动到底部
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadConversation(props.id);
|
await loadConversation(props.id);
|
||||||
const sessionid = generateSessionId(conversationInfo.userId, conversationInfo.targetId)
|
const sessionid = generateSessionId(conversationInfo.value.userId, conversationInfo.value.targetId)
|
||||||
await chatStore.swtichSession(sessionid,props.id);
|
await chatStore.swtichSession(sessionid,props.id);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
class="list-item" :class="{active: activeId === s.id}" @click="selectSession(s)">
|
class="list-item" :class="{active: activeId === s.id}" @click="selectSession(s)">
|
||||||
<div class="avatar-container">
|
<div class="avatar-container">
|
||||||
<img :src="s.targetAvatar ?? defaultAvatar" class="avatar-std" />
|
<img :src="s.targetAvatar ?? defaultAvatar" class="avatar-std" />
|
||||||
<span v-if="s.unread > 0" class="unread-badge">{{ s.unreadCount ?? 0 }}</span>
|
<span v-if="s.unreadCount > 0" class="unread-badge">{{ s.unreadCount ?? 0 }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="name-row">
|
<div class="name-row">
|
||||||
@@ -36,26 +36,19 @@ import { ref, computed, nextTick, onMounted } from 'vue'
|
|||||||
import { messageService } from '@/services/message'
|
import { messageService } from '@/services/message'
|
||||||
import defaultAvatar from '@/assets/default_avatar.png'
|
import defaultAvatar from '@/assets/default_avatar.png'
|
||||||
import { formatDate } from '@/utils/formatDate'
|
import { formatDate } from '@/utils/formatDate'
|
||||||
|
import { useConversationStore } from '@/stores/conversation'
|
||||||
|
|
||||||
|
const conversationStore = useConversationStore();
|
||||||
|
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const input = ref('')
|
|
||||||
const historyRef = ref(null)
|
|
||||||
const activeId = ref(1)
|
const activeId = ref(1)
|
||||||
const conversations = ref([]);
|
const conversations = ref([]);
|
||||||
|
|
||||||
const filteredSessions = computed(() => conversations.value.filter(s => s.targetName.includes(searchQuery.value)))
|
const filteredSessions = computed(() => conversationStore.conversations.filter(s => s.targetName.includes(searchQuery.value)))
|
||||||
|
|
||||||
const currentSession = computed(() => sessions.value.find(s => s.id === activeId.value))
|
|
||||||
|
|
||||||
function selectSession(s) {
|
function selectSession(s) {
|
||||||
activeId.value = s.id
|
activeId.value = s.id
|
||||||
router.push(`/messages/chat/${s.id}`)
|
router.push(`/messages/chat/${s.id}`)
|
||||||
scrollToBottom()
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollToBottom = async () => {
|
|
||||||
await nextTick()
|
|
||||||
if (historyRef.value) historyRef.value.scrollTop = historyRef.value.scrollHeight
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadConversation() {
|
async function loadConversation() {
|
||||||
@@ -65,7 +58,7 @@ async function loadConversation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadConversation();
|
await conversationStore.loadUserConversations();
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user