feat: merge alex version
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vocechat-web",
|
"name": "vocechat-web",
|
||||||
"version": "0.3.60",
|
"version": "0.3.61",
|
||||||
"homepage": "https://voce.chat",
|
"homepage": "https://voce.chat",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
|
|||||||
@@ -40,6 +40,8 @@
|
|||||||
|
|
||||||
"new_msg": "{{num}} new messages",
|
"new_msg": "{{num}} new messages",
|
||||||
"mark_read": "Mark As Read",
|
"mark_read": "Mark As Read",
|
||||||
|
"pin_chat": "Sticky on top",
|
||||||
|
"unpin_chat": "Unsticky",
|
||||||
|
|
||||||
"reply_msg_del": "This message has been deleted.",
|
"reply_msg_del": "This message has been deleted.",
|
||||||
|
|
||||||
@@ -54,5 +56,10 @@
|
|||||||
"unmute": "Unmute",
|
"unmute": "Unmute",
|
||||||
"camera_on": "Turn on camera",
|
"camera_on": "Turn on camera",
|
||||||
"camera_off": "Turn off camera",
|
"camera_off": "Turn off camera",
|
||||||
"leave_voice": "Leave Voice Chat"
|
"leave_voice": "Leave Voice Chat",
|
||||||
|
"add_contact": "Add to contacts",
|
||||||
|
"block": "Block",
|
||||||
|
"unblock": "Unblock",
|
||||||
|
"contact_tip": "This user is not in your contact",
|
||||||
|
"contact_block_tip": "This user has been blocked by you"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"new_private_channel": "New Private Channel",
|
"new_private_channel": "New Private Channel",
|
||||||
"new_msg": "New Message",
|
"new_msg": "New Message",
|
||||||
"invite_people": "Invite People",
|
"invite_people": "Invite People",
|
||||||
|
"search_people": "Search People",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"open": "Open",
|
"open": "Open",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
|
|||||||
@@ -13,14 +13,19 @@
|
|||||||
"new_pwd": "New Password",
|
"new_pwd": "New Password",
|
||||||
"confirm_new_pwd": "Confirm New Password",
|
"confirm_new_pwd": "Confirm New Password",
|
||||||
|
|
||||||
"manage_members": "Manage Members",
|
"manage_members": "Manage Contacts",
|
||||||
"manage_tip": "Disabling your account means you can recover it at any time after taking this action.",
|
"manage_tip": "Disabling your account means you can recover it at any time after taking this action.",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"copy_email": "Copy Email",
|
"copy_email": "Copy Email",
|
||||||
"send_msg": "Send Message",
|
"send_msg": "Send Message",
|
||||||
"remove": "Remove",
|
"remove": "Remove from Server",
|
||||||
|
"remove_from_contact": "Remove From Contact",
|
||||||
"remove_account": "Remove Account",
|
"remove_account": "Remove Account",
|
||||||
"remove_account_desc": "Are you sure remove this account?",
|
"remove_account_desc": "Are you sure remove this account?",
|
||||||
"remove_from_channel": "Remove From Channel"
|
"remove_from_channel": "Remove From Channel",
|
||||||
|
|
||||||
|
"search_not_found": "Not found, or this user is not allowed to be searched.",
|
||||||
|
"search_by_id": "Search by ID",
|
||||||
|
"search_by_email": "Search by Email"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"nav": {
|
"nav": {
|
||||||
"general": "General",
|
"general": "General",
|
||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
"members": "Members",
|
"members": "Contacts",
|
||||||
"auto_delete_msg": "Auto-delete Messages",
|
"auto_delete_msg": "Auto-delete Messages",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"my_account": "My Account",
|
"my_account": "My Account",
|
||||||
@@ -21,9 +21,7 @@
|
|||||||
"license": "License",
|
"license": "License",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"api_doc": "API Document",
|
"api_doc": "API Document",
|
||||||
"version": "Version",
|
"version": "Version"
|
||||||
"faq": "FAQ",
|
|
||||||
"feedback": "Feedback"
|
|
||||||
},
|
},
|
||||||
"channel": {
|
"channel": {
|
||||||
"leave": "Leave Channel",
|
"leave": "Leave Channel",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"title": "Welcome to {{name}} server",
|
"title": "Welcome to {{name}} server",
|
||||||
"desc": "Here are some steps to help you get started. For more, check out our Getting Started guide",
|
"desc": "Here are some steps to help you get started.",
|
||||||
"invite": "Invite your friends or teammates",
|
"invite": "Invite your friends or teammates",
|
||||||
"start_by_channel": "Create a Channel to Start a Conversation",
|
"start_by_channel": "Create a Channel to Start a Conversation",
|
||||||
"start_by_dm": "Send a Direct Message",
|
"start_by_dm": "Send a Direct Message",
|
||||||
|
|||||||
@@ -41,7 +41,8 @@
|
|||||||
|
|
||||||
"new_msg": "{{num}} 条新消息",
|
"new_msg": "{{num}} 条新消息",
|
||||||
"mark_read": "设为已读",
|
"mark_read": "设为已读",
|
||||||
|
"pin_chat": "置顶",
|
||||||
|
"unpin_chat": "取消置顶",
|
||||||
"reply_msg_del": "被回复的消息已删除!",
|
"reply_msg_del": "被回复的消息已删除!",
|
||||||
|
|
||||||
"file": "文件",
|
"file": "文件",
|
||||||
@@ -55,5 +56,10 @@
|
|||||||
"unmute": "取消静音",
|
"unmute": "取消静音",
|
||||||
"camera_on": "开启摄像头",
|
"camera_on": "开启摄像头",
|
||||||
"camera_off": "关闭摄像头",
|
"camera_off": "关闭摄像头",
|
||||||
"leave_voice": "离开"
|
"leave_voice": "离开",
|
||||||
|
"add_contact": "加为联系人",
|
||||||
|
"block": "屏蔽",
|
||||||
|
"unblock": "解除屏蔽",
|
||||||
|
"contact_tip": "该用户不在您的联系人列表中",
|
||||||
|
"contact_block_tip": "该用户已被你屏蔽"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"chat": "聊天",
|
"chat": "聊天",
|
||||||
"members": "成员",
|
"members": "联系人",
|
||||||
"favs": "收藏",
|
"favs": "收藏",
|
||||||
"files": "文件",
|
"files": "文件",
|
||||||
"setting": "设置",
|
"setting": "设置",
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"new_private_channel": "新建私有频道",
|
"new_private_channel": "新建私有频道",
|
||||||
"new_msg": "发消息",
|
"new_msg": "发消息",
|
||||||
"invite_people": "邀请TA人",
|
"invite_people": "邀请TA人",
|
||||||
|
"search_people": "搜索用户",
|
||||||
"search": "搜索",
|
"search": "搜索",
|
||||||
"create": "创建",
|
"create": "创建",
|
||||||
"open": "打开",
|
"open": "打开",
|
||||||
|
|||||||
@@ -12,14 +12,19 @@
|
|||||||
"current_pwd": "当前密码",
|
"current_pwd": "当前密码",
|
||||||
"new_pwd": "新密码",
|
"new_pwd": "新密码",
|
||||||
"confirm_new_pwd": "确认密码",
|
"confirm_new_pwd": "确认密码",
|
||||||
"manage_members": "成员管理",
|
"manage_members": "联系人管理",
|
||||||
"manage_tip": "请在此管理你的成员",
|
"manage_tip": "请在此管理你的联系人",
|
||||||
"admin": "管理员",
|
"admin": "管理员",
|
||||||
"user": "普通用户",
|
"user": "普通用户",
|
||||||
"copy_email": "复制邮箱",
|
"copy_email": "复制邮箱",
|
||||||
"send_msg": "发消息",
|
"send_msg": "发消息",
|
||||||
"remove": "删除",
|
"remove": "从服务器删除",
|
||||||
|
"remove_from_contact": "从联系人中移除",
|
||||||
"remove_account": "注销账号",
|
"remove_account": "注销账号",
|
||||||
"remove_account_desc": "确定永久注销该账号?",
|
"remove_account_desc": "确定永久注销该账号?",
|
||||||
"remove_from_channel": "从频道移除"
|
"remove_from_channel": "从频道移除",
|
||||||
|
|
||||||
|
"search_not_found": "用户不存在,或者不允许搜索",
|
||||||
|
"search_by_id": "用户ID",
|
||||||
|
"search_by_email": "用户邮箱"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"nav": {
|
"nav": {
|
||||||
"general": "通用",
|
"general": "通用",
|
||||||
"overview": "概况",
|
"overview": "概况",
|
||||||
"members": "成员",
|
"members": "联系人",
|
||||||
"auto_delete_msg": "自动删除消息",
|
"auto_delete_msg": "自动删除消息",
|
||||||
"my_account": "我的账号",
|
"my_account": "我的账号",
|
||||||
"config": "配置",
|
"config": "配置",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import localforage from "localforage";
|
import localforage from "localforage";
|
||||||
import { User } from "../../types/user";
|
import { ContactStatus, User } from "../../types/user";
|
||||||
import clearTable from "./clear.handler";
|
import clearTable from "./clear.handler";
|
||||||
|
|
||||||
export default async function handler({ operation, data, payload }) {
|
export default async function handler({ operation, data, payload }) {
|
||||||
@@ -18,6 +18,17 @@ export default async function handler({ operation, data, payload }) {
|
|||||||
await table.setItems(arr);
|
await table.setItems(arr);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "updateContactStatus":
|
||||||
|
{
|
||||||
|
const tmp = payload as { uid: number, status: ContactStatus } | { uid: number, status: ContactStatus }[];
|
||||||
|
const arr = Array.isArray(tmp) ? tmp : [tmp];
|
||||||
|
const opts = arr.map(({ uid, status }) => {
|
||||||
|
return { key: uid + "", value: { ...data.byId[uid], status } };
|
||||||
|
});
|
||||||
|
|
||||||
|
await table.setItems(opts);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "updateUsersByLogs":
|
case "updateUsersByLogs":
|
||||||
{
|
{
|
||||||
const changeLogs = payload;
|
const changeLogs = payload;
|
||||||
|
|||||||
@@ -102,8 +102,13 @@ const baseQueryWithTokenCheck = async (args: any, api: any, extraOptions: any) =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 403:
|
case 403: {
|
||||||
toast.error("Request Not Allowed");
|
const whiteList403 = ["sendMsg"];
|
||||||
|
if (!whiteList403.includes(api.endpoint)) {
|
||||||
|
toast.error("Request Not Allowed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case 404:
|
case 404:
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -117,6 +117,25 @@ export const channelApi = createApi({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
createPrivateInviteLink: builder.query<string, number | void>({
|
||||||
|
query: (gid) => ({
|
||||||
|
headers: {
|
||||||
|
"content-type": "text/plain",
|
||||||
|
accept: "text/plain"
|
||||||
|
},
|
||||||
|
// 七天过期
|
||||||
|
url: `/group/create_invite_private_magic_link?expired_in=604800&max_times=1&gid=${gid}`,
|
||||||
|
responseHandler: "text"
|
||||||
|
}),
|
||||||
|
transformResponse: (link: string) => {
|
||||||
|
// 确保http开头
|
||||||
|
const _link = link.startsWith("http") ? link : `http://${link}`;
|
||||||
|
// return _link;
|
||||||
|
// 替换掉域名
|
||||||
|
const invite = new URL(_link);
|
||||||
|
return `${location.origin}${invite.pathname}${invite.hash}${invite.search}`;
|
||||||
|
}
|
||||||
|
}),
|
||||||
removeChannel: builder.query<void, number>({
|
removeChannel: builder.query<void, number>({
|
||||||
query: (id) => ({
|
query: (id) => ({
|
||||||
url: `/group/${id}`,
|
url: `/group/${id}`,
|
||||||
@@ -186,6 +205,13 @@ export const channelApi = createApi({
|
|||||||
body: members
|
body: members
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
joinPrivateChannel: builder.mutation<Channel, { magic_token: string }>({
|
||||||
|
query: (body) => ({
|
||||||
|
url: `/user/join_private`,
|
||||||
|
method: "POST",
|
||||||
|
body
|
||||||
|
})
|
||||||
|
}),
|
||||||
updateIcon: builder.mutation<void, { gid: number; image: File }>({
|
updateIcon: builder.mutation<void, { gid: number; image: File }>({
|
||||||
query: ({ gid, image }) => ({
|
query: ({ gid, image }) => ({
|
||||||
headers: {
|
headers: {
|
||||||
@@ -216,6 +242,8 @@ export const {
|
|||||||
useChangeChannelTypeMutation,
|
useChangeChannelTypeMutation,
|
||||||
useLazyLeaveChannelQuery,
|
useLazyLeaveChannelQuery,
|
||||||
useLazyCreateInviteLinkQuery,
|
useLazyCreateInviteLinkQuery,
|
||||||
|
useJoinPrivateChannelMutation,
|
||||||
|
useLazyCreatePrivateInviteLinkQuery,
|
||||||
useCreateInviteLinkQuery,
|
useCreateInviteLinkQuery,
|
||||||
useGetChannelQuery,
|
useGetChannelQuery,
|
||||||
useLazyGetChannelQuery,
|
useLazyGetChannelQuery,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { batch } from "react-redux";
|
|||||||
import { ContentTypes } from "../config";
|
import { ContentTypes } from "../config";
|
||||||
import { addChannelMsg, removeChannelMsg } from "../slices/message.channel";
|
import { addChannelMsg, removeChannelMsg } from "../slices/message.channel";
|
||||||
import { addUserMsg, removeUserMsg } from "../slices/message.user";
|
import { addUserMsg, removeUserMsg } from "../slices/message.user";
|
||||||
import { addMessage, removeMessage } from "../slices/message";
|
import { addMessage, removeMessage, updateMessage } from "../slices/message";
|
||||||
// import { ContentType } from "../../types/message";
|
// import { ContentType } from "../../types/message";
|
||||||
|
|
||||||
export const onMessageSendStarted = async (
|
export const onMessageSendStarted = async (
|
||||||
@@ -57,10 +57,16 @@ export const onMessageSendStarted = async (
|
|||||||
dispatch(removeMessage(ts));
|
dispatch(removeMessage(ts));
|
||||||
}, 300);
|
}, 300);
|
||||||
// dispatch(removePendingMessage({ id, mid:ts, type: from }));
|
// dispatch(removePendingMessage({ id, mid:ts, type: from }));
|
||||||
} catch {
|
} catch (error) {
|
||||||
toast.error("Send Message Failed");
|
if (error?.error?.status == 403) {
|
||||||
dispatch(removeContextMessage({ id, mid: ts }));
|
// 403 means blocked
|
||||||
dispatch(removeMessage(ts));
|
// toast.error(`Failed to send, blocked maybe.`,);
|
||||||
|
dispatch(updateMessage({ mid: ts, failed: true }));
|
||||||
|
} else {
|
||||||
|
toast.error(`Send Message Failed ${JSON.stringify(error)}`,);
|
||||||
|
dispatch(removeContextMessage({ id, mid: ts }));
|
||||||
|
dispatch(removeMessage(ts));
|
||||||
|
}
|
||||||
// patchResult.undo();
|
// patchResult.undo();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,18 +2,19 @@ import { createApi } from "@reduxjs/toolkit/query/react";
|
|||||||
// import toast from "react-hot-toast";
|
// import toast from "react-hot-toast";
|
||||||
import baseQuery from "./base.query";
|
import baseQuery from "./base.query";
|
||||||
import { updateAutoDeleteSetting, updateMute } from "../slices/footprint";
|
import { updateAutoDeleteSetting, updateMute } from "../slices/footprint";
|
||||||
import { fillUsers } from "../slices/users";
|
import { fillUsers, updateContactStatus as updateStatus } from "../slices/users";
|
||||||
import BASE_URL, { ContentTypes } from "../config";
|
import BASE_URL, { ContentTypes } from "../config";
|
||||||
import { onMessageSendStarted } from "./handlers";
|
import { onMessageSendStarted } from "./handlers";
|
||||||
import { AutoDeleteMsgDTO, BotAPIKey, User, UserCreateDTO, UserDTO, UserForAdmin, UserForAdminDTO } from "../../types/user";
|
import { AutoDeleteMsgDTO, BotAPIKey, ContactAction, ContactResponse, ContactStatus, User, UserCreateDTO, UserDTO, UserForAdmin, UserForAdminDTO } from "../../types/user";
|
||||||
import { ContentTypeKey, MuteDTO } from "../../types/message";
|
import { ContentTypeKey, MuteDTO } from "../../types/message";
|
||||||
|
import { RootState } from "../store";
|
||||||
|
|
||||||
export const userApi = createApi({
|
export const userApi = createApi({
|
||||||
reducerPath: "userApi",
|
reducerPath: "userApi",
|
||||||
baseQuery,
|
baseQuery,
|
||||||
endpoints: (builder) => ({
|
endpoints: (builder) => ({
|
||||||
getUsers: builder.query<User[], void>({
|
getUsers: builder.query<User[], void>({
|
||||||
query: () => ({ url: `user` }),
|
query: () => ({ url: `/user` }),
|
||||||
transformResponse: (data: User[]) => {
|
transformResponse: (data: User[]) => {
|
||||||
return data.map((user) => {
|
return data.map((user) => {
|
||||||
return {
|
return {
|
||||||
@@ -25,12 +26,38 @@ export const userApi = createApi({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
async onQueryStarted(data, { dispatch, queryFulfilled, getState }) {
|
||||||
|
try {
|
||||||
|
const { data: users } = await queryFulfilled;
|
||||||
|
const { authData: { user: loginUser } } = getState() as RootState;
|
||||||
|
dispatch(fillUsers(users.map((u) => {
|
||||||
|
const status = loginUser?.uid == u.uid ? "added" : "";
|
||||||
|
return {
|
||||||
|
...u,
|
||||||
|
status
|
||||||
|
};
|
||||||
|
})));
|
||||||
|
} catch {
|
||||||
|
console.log("get user list error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
getContacts: builder.query<ContactResponse[], void>({
|
||||||
|
query: () => ({ url: `/user/contacts` }),
|
||||||
async onQueryStarted(data, { dispatch, queryFulfilled }) {
|
async onQueryStarted(data, { dispatch, queryFulfilled }) {
|
||||||
try {
|
try {
|
||||||
const { data: users } = await queryFulfilled;
|
const { data: users } = await queryFulfilled;
|
||||||
dispatch(fillUsers(users));
|
const payloads = users.map((c) => {
|
||||||
|
const uid = c.target_uid;
|
||||||
|
const status = c.contact_info.status;
|
||||||
|
return {
|
||||||
|
uid,
|
||||||
|
status
|
||||||
|
};
|
||||||
|
});
|
||||||
|
dispatch(updateStatus(payloads));
|
||||||
} catch {
|
} catch {
|
||||||
console.log("get user list error");
|
console.log("get contact list error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -44,6 +71,27 @@ export const userApi = createApi({
|
|||||||
method: "POST"
|
method: "POST"
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
|
searchUser: builder.mutation<User, { search_type: "id" | "email", keyword: string }>({
|
||||||
|
query: (input) => ({
|
||||||
|
url: `/user/search`,
|
||||||
|
body: input,
|
||||||
|
method: "POST"
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
pinChat: builder.mutation<void, { uid: number } | { gid: number }>({
|
||||||
|
query: (data) => ({
|
||||||
|
url: `/user/pin_chat`,
|
||||||
|
method: "POST",
|
||||||
|
body: { target: data }
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
unpinChat: builder.mutation<void, { uid: number } | { gid: number }>({
|
||||||
|
query: (data) => ({
|
||||||
|
url: `/user/unpin_chat`,
|
||||||
|
method: "POST",
|
||||||
|
body: { target: data }
|
||||||
|
})
|
||||||
|
}),
|
||||||
updateUser: builder.mutation<UserForAdmin, UserForAdminDTO>({
|
updateUser: builder.mutation<UserForAdmin, UserForAdminDTO>({
|
||||||
query: ({ id, ...rest }) => ({
|
query: ({ id, ...rest }) => ({
|
||||||
url: `/admin/user/${id}`,
|
url: `/admin/user/${id}`,
|
||||||
@@ -75,6 +123,28 @@ export const userApi = createApi({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
updateContactStatus: builder.mutation<void, { action: ContactAction, target_uid: number }>({
|
||||||
|
query: (payload) => ({
|
||||||
|
url: `/user/update_contact_status`,
|
||||||
|
method: "POST",
|
||||||
|
body: payload
|
||||||
|
}),
|
||||||
|
async onQueryStarted(data, { dispatch, queryFulfilled }) {
|
||||||
|
const map = {
|
||||||
|
"add": "added",
|
||||||
|
"block": "blocked",
|
||||||
|
"remove": "",
|
||||||
|
"unblock": "",
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await queryFulfilled;
|
||||||
|
const status = map[data.action] as ContactStatus;
|
||||||
|
dispatch(updateStatus({ uid: data.target_uid, status }));
|
||||||
|
} catch (error) {
|
||||||
|
console.log("update mute failed", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
updateMuteSetting: builder.mutation<void, MuteDTO>({
|
updateMuteSetting: builder.mutation<void, MuteDTO>({
|
||||||
query: (data) => ({
|
query: (data) => ({
|
||||||
url: `/user/mute`,
|
url: `/user/mute`,
|
||||||
@@ -152,6 +222,7 @@ export const userApi = createApi({
|
|||||||
body: type == "file" ? JSON.stringify(content) : content
|
body: type == "file" ? JSON.stringify(content) : content
|
||||||
}),
|
}),
|
||||||
async onQueryStarted(param1, param2) {
|
async onQueryStarted(param1, param2) {
|
||||||
|
// @ts-ignore
|
||||||
await onMessageSendStarted.call(this, param1, param2, "user");
|
await onMessageSendStarted.call(this, param1, param2, "user");
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
@@ -160,6 +231,7 @@ export const userApi = createApi({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
|
useLazyGetUsersQuery,
|
||||||
useGetUserByAdminQuery,
|
useGetUserByAdminQuery,
|
||||||
useUpdateAvatarByAdminMutation,
|
useUpdateAvatarByAdminMutation,
|
||||||
useUpdateAutoDeleteMsgMutation,
|
useUpdateAutoDeleteMsgMutation,
|
||||||
@@ -169,10 +241,13 @@ export const {
|
|||||||
useLazyDeleteUserQuery,
|
useLazyDeleteUserQuery,
|
||||||
useUpdateInfoMutation,
|
useUpdateInfoMutation,
|
||||||
useUpdateAvatarMutation,
|
useUpdateAvatarMutation,
|
||||||
useGetUsersQuery,
|
useLazyGetContactsQuery,
|
||||||
useLazyGetUsersQuery,
|
|
||||||
useSendMsgMutation,
|
useSendMsgMutation,
|
||||||
useCreateBotAPIKeyMutation,
|
useCreateBotAPIKeyMutation,
|
||||||
useLazyDeleteBotAPIKeyQuery,
|
useLazyDeleteBotAPIKeyQuery,
|
||||||
useGetBotAPIKeysQuery
|
useGetBotAPIKeysQuery,
|
||||||
|
useSearchUserMutation,
|
||||||
|
useUpdateContactStatusMutation,
|
||||||
|
usePinChatMutation,
|
||||||
|
useUnpinChatMutation
|
||||||
} = userApi;
|
} = userApi;
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const authDataSlice = createSlice({
|
|||||||
const { initialized = true, user, token, refresh_token, expired_in = 0 } = payload;
|
const { initialized = true, user, token, refresh_token, expired_in = 0 } = payload;
|
||||||
const { uid, create_by } = user;
|
const { uid, create_by } = user;
|
||||||
state.initialized = initialized;
|
state.initialized = initialized;
|
||||||
state.user = { ...state.user, ...user };
|
state.user = { ...state.user, ...user, status: "added" };
|
||||||
state.guest = create_by == "guest";
|
state.guest = create_by == "guest";
|
||||||
state.token = token;
|
state.token = token;
|
||||||
state.refreshToken = refresh_token;
|
state.refreshToken = refresh_token;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { MuteDTO } from "../../types/message";
|
import { MuteDTO } from "../../types/message";
|
||||||
import { OG } from "../../types/resource";
|
import { OG } from "../../types/resource";
|
||||||
import { AutoDeleteMessageSettingDTO, AutoDeleteMsgForGroup, AutoDeleteMsgForUser, AutoDeleteSettingForChannels, AutoDeleteSettingForUsers } from "../../types/sse";
|
import { AutoDeleteMessageSettingDTO, AutoDeleteMsgForGroup, AutoDeleteMsgForUser, AutoDeleteSettingForChannels, AutoDeleteSettingForUsers, PinChat, PinChatTarget } from "../../types/sse";
|
||||||
import { resetAuthData } from "./auth.data";
|
import { resetAuthData } from "./auth.data";
|
||||||
|
|
||||||
type ChannelAside = "members" | "voice" | "voice_fullscreen" | null;
|
type ChannelAside = "members" | "voice" | "voice_fullscreen" | null;
|
||||||
@@ -20,6 +20,7 @@ export interface State {
|
|||||||
autoDeleteMsgChannels: AutoDeleteMsgForGroup[];
|
autoDeleteMsgChannels: AutoDeleteMsgForGroup[];
|
||||||
channelAsides: { [cid: number]: ChannelAside };
|
channelAsides: { [cid: number]: ChannelAside };
|
||||||
dmAsides: { [uid: number]: DMAside };
|
dmAsides: { [uid: number]: DMAside };
|
||||||
|
pinChats: PinChat[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -36,7 +37,8 @@ const initialState: State = {
|
|||||||
autoDeleteMsgUsers: [],
|
autoDeleteMsgUsers: [],
|
||||||
autoDeleteMsgChannels: [],
|
autoDeleteMsgChannels: [],
|
||||||
channelAsides: {},
|
channelAsides: {},
|
||||||
dmAsides: {}
|
dmAsides: {},
|
||||||
|
pinChats: []
|
||||||
};
|
};
|
||||||
|
|
||||||
const footprintSlice = createSlice({
|
const footprintSlice = createSlice({
|
||||||
@@ -60,7 +62,8 @@ const footprintSlice = createSlice({
|
|||||||
autoDeleteMsgUsers = [],
|
autoDeleteMsgUsers = [],
|
||||||
autoDeleteMsgChannels = [],
|
autoDeleteMsgChannels = [],
|
||||||
channelAsides = {},
|
channelAsides = {},
|
||||||
dmAsides = {}
|
dmAsides = {},
|
||||||
|
pinChats = []
|
||||||
} = action.payload;
|
} = action.payload;
|
||||||
return {
|
return {
|
||||||
og,
|
og,
|
||||||
@@ -75,7 +78,8 @@ const footprintSlice = createSlice({
|
|||||||
autoDeleteMsgUsers,
|
autoDeleteMsgUsers,
|
||||||
autoDeleteMsgChannels,
|
autoDeleteMsgChannels,
|
||||||
channelAsides,
|
channelAsides,
|
||||||
dmAsides
|
dmAsides,
|
||||||
|
pinChats
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
updateUsersVersion(state, action: PayloadAction<number>) {
|
updateUsersVersion(state, action: PayloadAction<number>) {
|
||||||
@@ -164,6 +168,22 @@ const footprintSlice = createSlice({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
upsertPinChats(state, action: PayloadAction<{ pins: PinChat[], override?: boolean }>) {
|
||||||
|
const { pins, override = false } = action.payload;
|
||||||
|
if (override) {
|
||||||
|
state.pinChats = pins;
|
||||||
|
} else {
|
||||||
|
state.pinChats = [...pins, ...state.pinChats];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removePinChats(state, action: PayloadAction<PinChatTarget[]>) {
|
||||||
|
const pins = action.payload;
|
||||||
|
state.pinChats = state.pinChats.filter(pin => {
|
||||||
|
const key = "uid" in pin.target ? "uid" : "gid";
|
||||||
|
// @ts-ignore
|
||||||
|
return !pins.some(p => p[key] == pin.target[key]);
|
||||||
|
});
|
||||||
|
},
|
||||||
upsertOG(state, action: PayloadAction<{ key: string, value: OG }>) {
|
upsertOG(state, action: PayloadAction<{ key: string, value: OG }>) {
|
||||||
const { key, value } = action.payload;
|
const { key, value } = action.payload;
|
||||||
state.og[key] = value;
|
state.og[key] = value;
|
||||||
@@ -228,7 +248,9 @@ export const {
|
|||||||
updateHistoryMark,
|
updateHistoryMark,
|
||||||
upsertOG,
|
upsertOG,
|
||||||
updateChannelVisibleAside,
|
updateChannelVisibleAside,
|
||||||
updateDMVisibleAside
|
updateDMVisibleAside,
|
||||||
|
upsertPinChats,
|
||||||
|
removePinChats
|
||||||
} = footprintSlice.actions;
|
} = footprintSlice.actions;
|
||||||
|
|
||||||
export default footprintSlice.reducer;
|
export default footprintSlice.reducer;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface MessagePayload {
|
|||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
edited?: boolean | number;
|
edited?: boolean | number;
|
||||||
reply_mid?: number;
|
reply_mid?: number;
|
||||||
|
failed?: boolean;
|
||||||
}
|
}
|
||||||
export interface State {
|
export interface State {
|
||||||
[key: number]: MessagePayload;
|
[key: number]: MessagePayload;
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ export interface StoredServer extends Server {
|
|||||||
link: string;
|
link: string;
|
||||||
expire: number;
|
expire: number;
|
||||||
};
|
};
|
||||||
show_user_online_status: boolean,
|
|
||||||
webclient_auto_update: boolean
|
|
||||||
}
|
}
|
||||||
const initialState: StoredServer = {
|
const initialState: StoredServer = {
|
||||||
version: "",
|
version: "",
|
||||||
@@ -23,7 +21,7 @@ const initialState: StoredServer = {
|
|||||||
expire: 0
|
expire: 0
|
||||||
},
|
},
|
||||||
show_user_online_status: false,
|
show_user_online_status: false,
|
||||||
webclient_auto_update: true
|
webclient_auto_update: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const serverSlice = createSlice({
|
const serverSlice = createSlice({
|
||||||
@@ -45,7 +43,7 @@ const serverSlice = createSlice({
|
|||||||
name = "",
|
name = "",
|
||||||
description = "",
|
description = "",
|
||||||
show_user_online_status = true,
|
show_user_online_status = true,
|
||||||
webclient_auto_update = true
|
webclient_auto_update = true,
|
||||||
} = action.payload || {};
|
} = action.payload || {};
|
||||||
return { version, upgraded, name, logo, description, inviteLink, show_user_online_status, webclient_auto_update };
|
return { version, upgraded, name, logo, description, inviteLink, show_user_online_status, webclient_auto_update };
|
||||||
},
|
},
|
||||||
|
|||||||
+12
-4
@@ -1,11 +1,11 @@
|
|||||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||||
import { isNull, omitBy } from "lodash";
|
import { isNull, omitBy } from "lodash";
|
||||||
import BASE_URL from "../config";
|
import BASE_URL from "../config";
|
||||||
import { User } from "../../types/user";
|
import { Contact, ContactStatus } from "../../types/user";
|
||||||
import { UserLog, UserState } from "../../types/sse";
|
import { UserLog, UserState } from "../../types/sse";
|
||||||
|
|
||||||
type DMAside = "voice" | null;
|
type DMAside = "voice" | null;
|
||||||
export interface StoredUser extends User {
|
export interface StoredUser extends Contact {
|
||||||
online?: boolean;
|
online?: boolean;
|
||||||
voice?: boolean;
|
voice?: boolean;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
@@ -16,7 +16,6 @@ export interface State {
|
|||||||
ids: number[];
|
ids: number[];
|
||||||
byId: { [id: number]: StoredUser };
|
byId: { [id: number]: StoredUser };
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: State = {
|
const initialState: State = {
|
||||||
ids: [],
|
ids: [],
|
||||||
byId: {}
|
byId: {}
|
||||||
@@ -69,6 +68,7 @@ const usersSlice = createSlice({
|
|||||||
? ""
|
? ""
|
||||||
: `${BASE_URL}/resource/avatar?uid=${uid}&t=${rest.avatar_updated_at}`,
|
: `${BASE_URL}/resource/avatar?uid=${uid}&t=${rest.avatar_updated_at}`,
|
||||||
create_by: "", // todo: missing properties create_by
|
create_by: "", // todo: missing properties create_by
|
||||||
|
status: state.byId[uid]?.status ?? "",
|
||||||
...rest
|
...rest
|
||||||
};
|
};
|
||||||
const idx = state.ids.findIndex((i) => i == uid);
|
const idx = state.ids.findIndex((i) => i == uid);
|
||||||
@@ -98,9 +98,17 @@ const usersSlice = createSlice({
|
|||||||
state.byId[uid]!.online = online;
|
state.byId[uid]!.online = online;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
updateContactStatus(state, action: PayloadAction<{ uid: number, status: ContactStatus } | { uid: number, status: ContactStatus }[]>) {
|
||||||
|
const arr = Array.isArray(action.payload) ? action.payload : [action.payload];
|
||||||
|
arr.forEach((data) => {
|
||||||
|
if (state.byId[data.uid]) {
|
||||||
|
state.byId[data.uid]!.status = data.status;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { resetUsers, fillUsers, updateUsersByLogs, updateUsersStatus } = usersSlice.actions;
|
export const { updateContactStatus, resetUsers, fillUsers, updateUsersByLogs, updateUsersStatus } = usersSlice.actions;
|
||||||
export default usersSlice.reducer;
|
export default usersSlice.reducer;
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#101828" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.93 4.93L19.07 19.07M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 333 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg width="20" height="20" viewBox="0 0 20 20" stroke="#088AB2" fill="white" xmlns="http://www.w3.org/2000/svg">
|
<svg width="20" height="20" viewBox="0 0 20 20" stroke="#088AB2" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<g clip-path="url(#clip0_1130_82786)">
|
<g clip-path="url(#clip0_1130_82786)">
|
||||||
<path d="M9.99984 13.3334V10M9.99984 6.66669H10.0082M18.3332 10C18.3332 14.6024 14.6022 18.3334 9.99984 18.3334C5.39746 18.3334 1.6665 14.6024 1.6665 10C1.6665 5.39765 5.39746 1.66669 9.99984 1.66669C14.6022 1.66669 18.3332 5.39765 18.3332 10Z" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M9.99984 13.3334V10M9.99984 6.66669H10.0082M18.3332 10C18.3332 14.6024 14.6022 18.3334 9.99984 18.3334C5.39746 18.3334 1.6665 14.6024 1.6665 10C1.6665 5.39765 5.39746 1.66669 9.99984 1.66669C14.6022 1.66669 18.3332 5.39765 18.3332 10Z" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</g>
|
</g>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 575 B After Width: | Height: | Size: 574 B |
@@ -3,18 +3,21 @@ import { hideAll } from "tippy.js";
|
|||||||
import { useAppSelector } from "../../app/store";
|
import { useAppSelector } from "../../app/store";
|
||||||
import IconInvite from "../../assets/icons/add.person.svg";
|
import IconInvite from "../../assets/icons/add.person.svg";
|
||||||
import IconMention from "../../assets/icons/mention.svg";
|
import IconMention from "../../assets/icons/mention.svg";
|
||||||
|
import IconSearch from "../../assets/icons/search.svg";
|
||||||
import ChannelIcon from "./ChannelIcon";
|
import ChannelIcon from "./ChannelIcon";
|
||||||
import ChannelModal from "./ChannelModal";
|
import ChannelModal from "./ChannelModal";
|
||||||
import UsersModal from "./UsersModal";
|
import UsersModal from "./UsersModal";
|
||||||
import InviteModal from "./InviteModal";
|
import InviteModal from "./InviteModal";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import SearchUser from "./SearchUser";
|
||||||
|
import ServerVersionChecker from "./ServerVersionChecker";
|
||||||
|
|
||||||
export default function AddEntriesMenu() {
|
export default function AddEntriesMenu() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const currentUser = useAppSelector((store) => store.authData.user);
|
const currentUser = useAppSelector((store) => store.authData.user);
|
||||||
const [isPrivate, setIsPrivate] = useState(false);
|
const [isPrivate, setIsPrivate] = useState(false);
|
||||||
const [inviteModalVisible, setInviteModalVisible] = useState(false);
|
const [inviteModalVisible, setInviteModalVisible] = useState(false);
|
||||||
|
const [searchModalVisible, setSearchModalVisible] = useState(false);
|
||||||
const [channelModalVisible, setChannelModalVisible] = useState(false);
|
const [channelModalVisible, setChannelModalVisible] = useState(false);
|
||||||
const [usersModalVisible, setUsersModalVisible] = useState(false);
|
const [usersModalVisible, setUsersModalVisible] = useState(false);
|
||||||
const toggleInviteModalVisible = () => {
|
const toggleInviteModalVisible = () => {
|
||||||
@@ -25,6 +28,14 @@ export default function AddEntriesMenu() {
|
|||||||
return !prev;
|
return !prev;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
const toggleSearchModalVisible = () => {
|
||||||
|
setSearchModalVisible((prevVisible) => {
|
||||||
|
if (!prevVisible) {
|
||||||
|
hideAll();
|
||||||
|
}
|
||||||
|
return !prevVisible;
|
||||||
|
});
|
||||||
|
};
|
||||||
const toggleUsersModalVisible = () => {
|
const toggleUsersModalVisible = () => {
|
||||||
setUsersModalVisible((prevVisible) => {
|
setUsersModalVisible((prevVisible) => {
|
||||||
if (!prevVisible) {
|
if (!prevVisible) {
|
||||||
@@ -66,10 +77,17 @@ export default function AddEntriesMenu() {
|
|||||||
<IconInvite className={iconClass} />
|
<IconInvite className={iconClass} />
|
||||||
{t("action.invite_people")}
|
{t("action.invite_people")}
|
||||||
</li>
|
</li>
|
||||||
|
<ServerVersionChecker version="0.3.7" empty={true}>
|
||||||
|
<li className={itemClass} onClick={toggleSearchModalVisible}>
|
||||||
|
<IconSearch className={iconClass} />
|
||||||
|
{t("action.search_people")}
|
||||||
|
</li>
|
||||||
|
</ServerVersionChecker>
|
||||||
</ul>
|
</ul>
|
||||||
{channelModalVisible && <ChannelModal personal={isPrivate} closeModal={handleCloseModal} />}
|
{channelModalVisible && <ChannelModal personal={isPrivate} closeModal={handleCloseModal} />}
|
||||||
{usersModalVisible && <UsersModal closeModal={toggleUsersModalVisible} />}
|
{usersModalVisible && <UsersModal closeModal={toggleUsersModalVisible} />}
|
||||||
{inviteModalVisible && <InviteModal closeModal={toggleInviteModalVisible} />}
|
{inviteModalVisible && <InviteModal closeModal={toggleInviteModalVisible} />}
|
||||||
|
{searchModalVisible && <SearchUser closeModal={toggleSearchModalVisible} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,13 @@ import useMessageOperation from "./useMessageOperation";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
type Props = {
|
type Props = {
|
||||||
|
isSelf: boolean;
|
||||||
context: "user" | "channel";
|
context: "user" | "channel";
|
||||||
contextId: number;
|
contextId: number;
|
||||||
mid: number;
|
mid: number;
|
||||||
toggleEditMessage: () => void;
|
toggleEditMessage: () => void;
|
||||||
};
|
};
|
||||||
const Commands: FC<Props> = ({ context = "user", contextId = 0, mid = 0, toggleEditMessage }) => {
|
const Commands: FC<Props> = ({ isSelf, context = "user", contextId = 0, mid = 0, toggleEditMessage }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
canDelete,
|
canDelete,
|
||||||
@@ -87,7 +88,10 @@ const Commands: FC<Props> = ({ context = "user", contextId = 0, mid = 0, toggleE
|
|||||||
<>
|
<>
|
||||||
<ul
|
<ul
|
||||||
ref={cmdsRef}
|
ref={cmdsRef}
|
||||||
className={clsx(`bg-white dark:bg-gray-900 rounded-md z-[999] absolute right-2.5 top-0 -translate-y-1/2 flex items-center border border-solid border-black/10 invisible group-hover:visible`, tippyVisible && '!visible')}>
|
className={clsx(`bg-white dark:bg-gray-900 rounded-md z-[999] absolute top-0 -translate-y-1/2 flex items-center border border-solid border-black/10 invisible group-hover:visible`,
|
||||||
|
tippyVisible && '!visible',
|
||||||
|
isSelf ? "left-2.5" : "right-2.5"
|
||||||
|
)}>
|
||||||
<Tippy
|
<Tippy
|
||||||
onShow={handleTippyVisible.bind(null, true)}
|
onShow={handleTippyVisible.bind(null, true)}
|
||||||
onHide={handleTippyVisible.bind(null, false)}
|
onHide={handleTippyVisible.bind(null, false)}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import useContextMenu from "../../hook/useContextMenu";
|
|||||||
import usePinMessage from "../../hook/usePinMessage";
|
import usePinMessage from "../../hook/usePinMessage";
|
||||||
import { useAppSelector } from "../../../app/store";
|
import { useAppSelector } from "../../../app/store";
|
||||||
import ExpireTimer from "./ExpireTimer";
|
import ExpireTimer from "./ExpireTimer";
|
||||||
|
import IconInfo from '../../../assets/icons/info.svg';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
@@ -39,11 +40,12 @@ const Message: FC<IProps> = ({
|
|||||||
const [edit, setEdit] = useState(false);
|
const [edit, setEdit] = useState(false);
|
||||||
const avatarRef = useRef(null);
|
const avatarRef = useRef(null);
|
||||||
const { getPinInfo } = usePinMessage(context == "channel" ? contextId : 0);
|
const { getPinInfo } = usePinMessage(context == "channel" ? contextId : 0);
|
||||||
const { message, reactionMessageData, usersData } = useAppSelector((store) => {
|
const { message, reactionMessageData, usersData, loginUid } = useAppSelector((store) => {
|
||||||
return {
|
return {
|
||||||
reactionMessageData: store.reactionMessage,
|
reactionMessageData: store.reactionMessage,
|
||||||
message: store.message[mid],
|
message: store.message[mid],
|
||||||
usersData: store.users.byId
|
usersData: store.users.byId,
|
||||||
|
loginUid: store.authData.user?.uid
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,7 +77,9 @@ const Message: FC<IProps> = ({
|
|||||||
content_type = "text/plain",
|
content_type = "text/plain",
|
||||||
edited,
|
edited,
|
||||||
properties,
|
properties,
|
||||||
expires_in = 0
|
expires_in = 0,
|
||||||
|
failed = false
|
||||||
|
|
||||||
} = message;
|
} = message;
|
||||||
|
|
||||||
|
|
||||||
@@ -90,6 +94,7 @@ const Message: FC<IProps> = ({
|
|||||||
// return null;
|
// return null;
|
||||||
const _key = properties?.local_id || mid;
|
const _key = properties?.local_id || mid;
|
||||||
const showExpire = (expires_in ?? 0) > 0;
|
const showExpire = (expires_in ?? 0) > 0;
|
||||||
|
const isSelf = fromUid == loginUid;
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -100,7 +105,8 @@ const Message: FC<IProps> = ({
|
|||||||
className={clsx(`group w-full relative flex items-start gap-2 md:gap-4 p-1 md:p-2 my-2 rounded-lg md:dark:hover:bg-gray-800 md:hover:bg-gray-100`,
|
className={clsx(`group w-full relative flex items-start gap-2 md:gap-4 p-1 md:p-2 my-2 rounded-lg md:dark:hover:bg-gray-800 md:hover:bg-gray-100`,
|
||||||
readOnly && "hover:bg-transparent",
|
readOnly && "hover:bg-transparent",
|
||||||
showExpire && "bg-red-200 dark:bg-red-200/40",
|
showExpire && "bg-red-200 dark:bg-red-200/40",
|
||||||
pinInfo && "bg-cyan-50 dark:bg-cyan-800 pt-7"
|
pinInfo && "bg-cyan-50 dark:bg-cyan-800 pt-7",
|
||||||
|
isSelf && "flex-row-reverse",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Tippy
|
<Tippy
|
||||||
@@ -121,18 +127,18 @@ const Message: FC<IProps> = ({
|
|||||||
context={context}
|
context={context}
|
||||||
contextId={contextId}
|
contextId={contextId}
|
||||||
mid={mid}
|
mid={mid}
|
||||||
visible={contextMenuVisible}
|
visible={contextMenuVisible && !failed}
|
||||||
hide={hideContextMenu}
|
hide={hideContextMenu}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx("w-full flex flex-col gap-2", pinInfo && "relative")}
|
className={clsx("w-full flex flex-col gap-2", pinInfo && "relative", isSelf && "items-end")}
|
||||||
data-pin-tip={`pinned by ${pinInfo?.created_by ? usersData[pinInfo.created_by]?.name : ""
|
data-pin-tip={`pinned by ${pinInfo?.created_by ? usersData[pinInfo.created_by]?.name : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{pinInfo && <span className="absolute left-0 -top-1 -translate-y-full text-xs text-gray-400">
|
{pinInfo && <span className="absolute left-0 -top-1 -translate-y-full text-xs text-gray-400">
|
||||||
{`pinned by ${pinInfo.created_by ? usersData[pinInfo.created_by]?.name : ""}`}
|
{`pinned by ${pinInfo.created_by ? usersData[pinInfo.created_by]?.name : ""}`}
|
||||||
</span>}
|
</span>}
|
||||||
<div className="flex items-center gap-2 font-semibold">
|
<div className={clsx(`flex items-center gap-2 font-semibold`, isSelf && "flex-row-reverse")}>
|
||||||
<span className="text-primary-500 text-sm">{currUser?.name || "Deleted User"}</span>
|
<span className="text-primary-500 text-sm">{currUser?.name || "Deleted User"}</span>
|
||||||
<Tooltip
|
<Tooltip
|
||||||
delay={200}
|
delay={200}
|
||||||
@@ -146,6 +152,9 @@ const Message: FC<IProps> = ({
|
|||||||
: dayjsTime.format("YYYY-MM-DD h:mm:ss A")}
|
: dayjsTime.format("YYYY-MM-DD h:mm:ss A")}
|
||||||
</time>
|
</time>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{failed && <span className="text-red-500 text-xs flex items-center gap-1">
|
||||||
|
<IconInfo className="stroke-red-600 w-4 h-4" /> Send Failed
|
||||||
|
</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx(`select-text text-gray-800 text-sm break-all whitespace-pre-wrap dark:!text-white`,
|
<div className={clsx(`select-text text-gray-800 text-sm break-all whitespace-pre-wrap dark:!text-white`,
|
||||||
sending && "opacity-90",
|
sending && "opacity-90",
|
||||||
@@ -181,8 +190,9 @@ const Message: FC<IProps> = ({
|
|||||||
createAt={time ?? 0}
|
createAt={time ?? 0}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!edit && !readOnly && (
|
{!edit && !failed && !readOnly && (
|
||||||
<Commands
|
<Commands
|
||||||
|
isSelf={isSelf}
|
||||||
context={context}
|
context={context}
|
||||||
contextId={contextId}
|
contextId={contextId}
|
||||||
mid={mid}
|
mid={mid}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useEffect, useState, useCallback, ClipboardEvent, FC } from "react";
|
import { useRef, useEffect, ClipboardEvent, FC } from "react";
|
||||||
import { useKey } from "rooks";
|
import { useKey } from "rooks";
|
||||||
import { Editor, Transforms } from "slate";
|
import { Editor, Transforms } from "slate";
|
||||||
import {
|
import {
|
||||||
@@ -37,6 +37,7 @@ type Props = {
|
|||||||
id: `${ctx}_${number}`;
|
id: `${ctx}_${number}`;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
sendMessages: any;
|
sendMessages: any;
|
||||||
|
updateMessages: any;
|
||||||
members: number[];
|
members: number[];
|
||||||
};
|
};
|
||||||
const Plugins: FC<Props> = ({
|
const Plugins: FC<Props> = ({
|
||||||
@@ -45,6 +46,7 @@ const Plugins: FC<Props> = ({
|
|||||||
id,
|
id,
|
||||||
placeholder = "Write some markdown...",
|
placeholder = "Write some markdown...",
|
||||||
sendMessages,
|
sendMessages,
|
||||||
|
updateMessages,
|
||||||
members = []
|
members = []
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
@@ -53,7 +55,6 @@ const Plugins: FC<Props> = ({
|
|||||||
const { addStageFile } = useUploadFile({ context, id: to });
|
const { addStageFile } = useUploadFile({ context, id: to });
|
||||||
const enableMentions = members.length > 0;
|
const enableMentions = members.length > 0;
|
||||||
const userData = useAppSelector((store) => store.users.byId);
|
const userData = useAppSelector((store) => store.users.byId);
|
||||||
const [msgs, setMsgs] = useState([]);
|
|
||||||
const editableRef = useRef(null);
|
const editableRef = useRef(null);
|
||||||
const initialProps = {
|
const initialProps = {
|
||||||
...CONFIG.editableProps,
|
...CONFIG.editableProps,
|
||||||
@@ -94,23 +95,13 @@ const Plugins: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
// return true;
|
// return true;
|
||||||
handleSend();
|
sendMessages();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
when: !isMobile(),
|
when: !isMobile(),
|
||||||
target: editableRef,
|
target: editableRef,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const handleSend = () => {
|
|
||||||
sendMessages(msgs);
|
|
||||||
// 清空
|
|
||||||
Transforms.delete(plateEditor, {
|
|
||||||
at: {
|
|
||||||
anchor: Editor.start(plateEditor, []),
|
|
||||||
focus: Editor.end(plateEditor, [])
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
const pluginArr = [
|
const pluginArr = [
|
||||||
createParagraphPlugin(),
|
createParagraphPlugin(),
|
||||||
createNodeIdPlugin(),
|
createNodeIdPlugin(),
|
||||||
@@ -142,55 +133,52 @@ const Plugins: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = (val: any) => {
|
||||||
async (val: any) => {
|
// console.log("tmps changed", val);
|
||||||
// console.log("tmps changed", val);
|
const tmps = [];
|
||||||
const tmps = [];
|
const getMixedText = (children: any) => {
|
||||||
const getMixedText = (children: any) => {
|
const mentions: any = [];
|
||||||
const mentions: any = [];
|
const arr = children.map(({ type, text, uid }: any) => {
|
||||||
const arr = children.map(({ type, text, uid }: any) => {
|
if (type == "mention") {
|
||||||
if (type == "mention") {
|
mentions.push(uid);
|
||||||
mentions.push(uid);
|
return ` @${uid} `;
|
||||||
return ` @${uid} `;
|
}
|
||||||
}
|
return text;
|
||||||
return text;
|
});
|
||||||
});
|
return { value: arr.join(""), mentions };
|
||||||
return { value: arr.join(""), mentions };
|
};
|
||||||
};
|
for (const v of val) {
|
||||||
for (const v of val) {
|
// p
|
||||||
// p
|
const { value, mentions } = getMixedText(v.children);
|
||||||
const { value, mentions } = getMixedText(v.children);
|
const prev = tmps[tmps.length - 1];
|
||||||
const prev = tmps[tmps.length - 1];
|
if (!prev) {
|
||||||
if (!prev) {
|
tmps.push([{ type: "text", content: value, properties: { mentions } }]);
|
||||||
tmps.push([{ type: "text", content: value, properties: { mentions } }]);
|
} else {
|
||||||
|
if (Array.isArray(prev)) {
|
||||||
|
tmps[tmps.length - 1].push({
|
||||||
|
type: "text",
|
||||||
|
content: value,
|
||||||
|
properties: { mentions }
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
if (Array.isArray(prev)) {
|
tmps.push([{ type: "text", content: value, properties: { mentions } }]);
|
||||||
tmps[tmps.length - 1].push({
|
|
||||||
type: "text",
|
|
||||||
content: value,
|
|
||||||
properties: { mentions }
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
tmps.push([{ type: "text", content: value, properties: { mentions } }]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const arr = tmps.map((tmp) => {
|
}
|
||||||
return Array.isArray(tmp)
|
const arr = tmps.map((tmp) => {
|
||||||
? {
|
return Array.isArray(tmp)
|
||||||
type: "text",
|
? {
|
||||||
content: tmp.map((t) => t.content).join("\n"),
|
type: "text",
|
||||||
properties: {
|
content: tmp.map((t) => t.content).join("\n"),
|
||||||
mentions: tmp.map((t) => t.properties?.mentions || []).flat()
|
properties: {
|
||||||
}
|
mentions: tmp.map((t) => t.properties?.mentions || []).flat()
|
||||||
}
|
}
|
||||||
: tmp;
|
}
|
||||||
});
|
: tmp;
|
||||||
const msgs = arr.filter(({ content }) => !!content);
|
});
|
||||||
setMsgs(msgs);
|
const msgs = arr.filter(({ content }) => !!content.trim());
|
||||||
},
|
updateMessages(msgs);
|
||||||
[msgs]
|
};
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -245,7 +233,18 @@ export const useMixedEditor = (key: string) => {
|
|||||||
editorRef.insertText(txt);
|
editorRef.insertText(txt);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const resetInput = () => {
|
||||||
|
if (!editorRef) return;
|
||||||
|
// 清空
|
||||||
|
Transforms.delete(editorRef, {
|
||||||
|
at: {
|
||||||
|
anchor: Editor.start(editorRef, []),
|
||||||
|
focus: Editor.end(editorRef, [])
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
return {
|
return {
|
||||||
|
resetInput,
|
||||||
focus,
|
focus,
|
||||||
insertText
|
insertText
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { FC, ReactElement } from "react";
|
||||||
|
import useTabBroadcast from "../hook/useTabBroadcast";
|
||||||
|
import InactiveScreen from "./InactiveScreen";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactElement;
|
||||||
|
}
|
||||||
|
const RequireSingleTab: FC<Props> = ({ children }) => {
|
||||||
|
const { tabActive } = useTabBroadcast();
|
||||||
|
// console.log("authhhhh", allowGuest, token, guest);
|
||||||
|
return tabActive ? children : <InactiveScreen />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RequireSingleTab;
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// import Tippy from "@tippyjs/react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { FC, ChangeEvent, useState, useRef, FormEvent } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import Modal from "./Modal";
|
||||||
|
import IconClose from "../../assets/icons/close.svg";
|
||||||
|
import IconSearch from "../../assets/icons/search.svg";
|
||||||
|
|
||||||
|
import { useSearchUserMutation, useUpdateContactStatusMutation } from "../../app/services/user";
|
||||||
|
import StyledButton from "./styled/Button";
|
||||||
|
import Input from "./styled/Input";
|
||||||
|
import Avatar from "./Avatar";
|
||||||
|
import BASE_URL from "../../app/config";
|
||||||
|
import { useAppSelector } from "../../app/store";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
type Props = {
|
||||||
|
closeModal: () => void;
|
||||||
|
};
|
||||||
|
type Type = "id" | "email";
|
||||||
|
const SearchUser: FC<Props> = ({ closeModal }) => {
|
||||||
|
const [updateContactStatus, { isLoading: adding }] = useUpdateContactStatusMutation();
|
||||||
|
const usersData = useAppSelector(store => store.users.byId);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigateTo = useNavigate();
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
const [type, setType] = useState<Type>("id");
|
||||||
|
const [input, setInput] = useState({
|
||||||
|
id: "",
|
||||||
|
email: ""
|
||||||
|
});
|
||||||
|
const [searchUser, { data, isSuccess, isLoading, reset }] = useSearchUserMutation();
|
||||||
|
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const tmp = {
|
||||||
|
[type]: evt.target.value
|
||||||
|
};
|
||||||
|
setInput(prev => ({ ...prev, ...tmp }));
|
||||||
|
};
|
||||||
|
const resetInput = () => {
|
||||||
|
reset();
|
||||||
|
setInput(prev => ({ ...prev, [type]: "" }));
|
||||||
|
};
|
||||||
|
const handleSubmit = (evt: FormEvent<HTMLFormElement>) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
if (!inputRef.current) return;
|
||||||
|
const form = inputRef.current as HTMLFormElement;
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
form.reportValidity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const currInput = input[type];
|
||||||
|
// if (!currInput) return;
|
||||||
|
// return true;
|
||||||
|
searchUser({
|
||||||
|
search_type: type,
|
||||||
|
keyword: currInput
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeKeyword = (type: Type) => {
|
||||||
|
setType(type);
|
||||||
|
};
|
||||||
|
const handleChat = async (directChat: boolean) => {
|
||||||
|
if (!data) return;
|
||||||
|
if (!directChat) {
|
||||||
|
await updateContactStatus({ target_uid: data.uid, action: "add" });
|
||||||
|
}
|
||||||
|
closeModal();
|
||||||
|
navigateTo(`/chat/dm/${data.uid}`);
|
||||||
|
};
|
||||||
|
// const tmpData = { "avatar_updated_at": 0, "birthday": 0, "create_by": "password", "email": "tristan@alex.com", "gender": 0, "is_admin": true, "is_bot": false, "language": "en-US", "name": "Admin", "uid": 1 };
|
||||||
|
// const showResult = isSuccess && data;
|
||||||
|
const inContact = Boolean(data && usersData[data.uid] && usersData[data.uid].status == "added");
|
||||||
|
const inputType = type == "id" ? "number" : "email";
|
||||||
|
return (
|
||||||
|
<Modal>
|
||||||
|
<div className=" relative flex flex-col gap-2 w-96 px-4 py-3 rounded-lg bg-gray-100 dark:bg-gray-900 text-slate-900 dark:text-slate-100">
|
||||||
|
<div className="flex items-center gap-2 py-2">
|
||||||
|
<StyledButton className={clsx("mini", type == "email" && "ghost !border-none !shadow-none")} onClick={handleChangeKeyword.bind(null, "id")}>
|
||||||
|
{t("search_by_id", { ns: "member" })}
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton className={clsx("mini", type == "id" && "ghost !border-none !shadow-none")} onClick={handleChangeKeyword.bind(null, "email")}>
|
||||||
|
{t("search_by_email", { ns: "member" })}
|
||||||
|
</StyledButton>
|
||||||
|
</div>
|
||||||
|
<form className="w-full" ref={inputRef} action="/" onSubmit={handleSubmit}>
|
||||||
|
<Input required type={inputType} className="none" disabled={isLoading} prefix={<IconSearch className="dark:fill-gray-400 w-6 h-6 shrink-0" />} value={input[type]} placeholder={`${t("action.search")}...`} onChange={handleInput} />
|
||||||
|
</form>
|
||||||
|
<div className="min-h-[280px] flex-center pb-10">
|
||||||
|
{isSuccess ? (data ? <div className="flex flex-col items-center pt-10">
|
||||||
|
<Avatar className="rounded-full" src={data.avatar_updated_at === 0 ? "" : `${BASE_URL}/resource/avatar?uid=${data.uid}&t=${data.avatar_updated_at}`} name={data.name} width={120} height={120} />
|
||||||
|
<span className="my-2 dark:text-gray-100 text-gray-950">{data.name}</span>
|
||||||
|
<div className="flex gap-2 my-2">
|
||||||
|
<StyledButton className="mini ghost" onClick={resetInput}>{t("action.cancel")}</StyledButton>
|
||||||
|
<StyledButton disabled={adding} onClick={handleChat.bind(null, inContact)} className="mini">{inContact ? t(`chat`) : `Add to Contact`}</StyledButton>
|
||||||
|
</div>
|
||||||
|
</div> : <div className="w-full h-full text-center flex flex-col gap-3 items-center">
|
||||||
|
<span className="text-sm text-gray-800 dark:text-gray-200">
|
||||||
|
{t("search_not_found", { ns: "member" })}
|
||||||
|
</span>
|
||||||
|
<StyledButton className="mini" onClick={resetInput}>Ok</StyledButton>
|
||||||
|
</div>) : null}
|
||||||
|
</div>
|
||||||
|
<IconClose role="button" className="absolute top-2 right-2 dark:fill-white w-5 h-5" onClick={closeModal} />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SearchUser;
|
||||||
@@ -2,20 +2,25 @@ import { ChangeEvent, useRef, FC } from "react";
|
|||||||
import Tooltip from "../Tooltip";
|
import Tooltip from "../Tooltip";
|
||||||
import AddIcon from "../../../assets/icons/add.solid.svg";
|
import AddIcon from "../../../assets/icons/add.solid.svg";
|
||||||
import MarkdownIcon from "../../../assets/icons/markdown.svg";
|
import MarkdownIcon from "../../../assets/icons/markdown.svg";
|
||||||
|
import SendIcon from "../../../assets/icons/send.svg";
|
||||||
import FullscreenIcon from "../../../assets/icons/fullscreen.svg";
|
import FullscreenIcon from "../../../assets/icons/fullscreen.svg";
|
||||||
import ExitFullscreenIcon from "../../../assets/icons/fullscreen.exit.svg";
|
import ExitFullscreenIcon from "../../../assets/icons/fullscreen.exit.svg";
|
||||||
import useUploadFile from "../../hook/useUploadFile";
|
import useUploadFile from "../../hook/useUploadFile";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
sendMessages: () => void;
|
||||||
toggleMarkdownFullscreen: () => void;
|
toggleMarkdownFullscreen: () => void;
|
||||||
fullscreen: boolean;
|
fullscreen: boolean;
|
||||||
toggleMode: () => void;
|
toggleMode: () => void;
|
||||||
mode: "markdown" | "text";
|
mode: "markdown" | "text";
|
||||||
to: number;
|
to: number;
|
||||||
context: "user" | "channel";
|
context: "user" | "channel";
|
||||||
|
sendVisible: boolean
|
||||||
};
|
};
|
||||||
const Toolbar: FC<Props> = ({
|
const Toolbar: FC<Props> = ({
|
||||||
|
sendMessages,
|
||||||
|
sendVisible,
|
||||||
toggleMarkdownFullscreen,
|
toggleMarkdownFullscreen,
|
||||||
fullscreen,
|
fullscreen,
|
||||||
toggleMode,
|
toggleMode,
|
||||||
@@ -61,7 +66,7 @@ const Toolbar: FC<Props> = ({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{!isMarkdown && <Tooltip placement="top" tip={t("action.upload")}>
|
{!isMarkdown && <><Tooltip placement="top" tip={t("action.upload")}>
|
||||||
<div className="cursor-pointer relative w-6 h-6">
|
<div className="cursor-pointer relative w-6 h-6">
|
||||||
<AddIcon className="dark:fill-gray-300" />
|
<AddIcon className="dark:fill-gray-300" />
|
||||||
<label htmlFor="file" className=" cursor-pointer absolute left-0 top-0 w-full h-full opacity-0">
|
<label htmlFor="file" className=" cursor-pointer absolute left-0 top-0 w-full h-full opacity-0">
|
||||||
@@ -77,7 +82,11 @@ const Toolbar: FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</Tooltip>}
|
</Tooltip>
|
||||||
|
{sendVisible && <Tooltip placement="top" tip="Send">
|
||||||
|
<SendIcon className={"w-6 h-6 cursor-pointer animate-zoomIn dark:stroke-gray-300"} onClick={sendMessages} />
|
||||||
|
</Tooltip>}
|
||||||
|
</>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import useDraft from "../../hook/useDraft";
|
|||||||
import useUploadFile from "../../hook/useUploadFile";
|
import useUploadFile from "../../hook/useUploadFile";
|
||||||
import { useAppDispatch, useAppSelector } from "../../../app/store";
|
import { useAppDispatch, useAppSelector } from "../../../app/store";
|
||||||
import TextInput from "../TextInput";
|
import TextInput from "../TextInput";
|
||||||
|
import useUserOperation from "../../hook/useUserOperation";
|
||||||
|
import StyledButton from "../styled/Button";
|
||||||
|
|
||||||
const Modes = {
|
const Modes = {
|
||||||
text: "text",
|
text: "text",
|
||||||
@@ -33,9 +35,11 @@ const Send: FC<IProps> = ({
|
|||||||
id
|
id
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation("chat");
|
const { t } = useTranslation("chat");
|
||||||
|
const { unblockThisContact, blocked } = useUserOperation({ uid: context == "user" ? id : undefined, cid: context == "channel" ? id : undefined });
|
||||||
const { resetStageFiles } = useUploadFile({ context, id });
|
const { resetStageFiles } = useUploadFile({ context, id });
|
||||||
const { getDraft, getUpdateDraft } = useDraft({ context, id });
|
const { getDraft, getUpdateDraft } = useDraft({ context, id });
|
||||||
const editor = useMixedEditor(`${context}_${id}`);
|
const editor = useMixedEditor(`${context}_${id}`);
|
||||||
|
const [msgs, setMsgs] = useState([]);
|
||||||
const [markdownEditor, setMarkdownEditor] = useState(null);
|
const [markdownEditor, setMarkdownEditor] = useState(null);
|
||||||
const [markdownFullscreen, setMarkdownFullscreen] = useState(false);
|
const [markdownFullscreen, setMarkdownFullscreen] = useState(false);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@@ -57,7 +61,7 @@ const Send: FC<IProps> = ({
|
|||||||
mode: store.ui.inputMode,
|
mode: store.ui.inputMode,
|
||||||
from_uid: store.authData.user?.uid,
|
from_uid: store.authData.user?.uid,
|
||||||
replying_mid: store.message.replying[`${context}_${id}`],
|
replying_mid: store.message.replying[`${context}_${id}`],
|
||||||
uploadFiles: store.ui.uploadFiles[`${context}_${id}`]
|
uploadFiles: store.ui.uploadFiles[`${context}_${id}`] ?? []
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const { sendMessage } = useSendMessage({ context, from: from_uid, to: id });
|
const { sendMessage } = useSendMessage({ context, from: from_uid, to: id });
|
||||||
@@ -76,12 +80,14 @@ const Send: FC<IProps> = ({
|
|||||||
editor.insertText(emoji);
|
editor.insertText(emoji);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleSendMessage = async (msgs = []) => {
|
const handleSendMessage = async () => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
editor.resetInput();
|
||||||
if (msgs && msgs.length) {
|
if (msgs && msgs.length) {
|
||||||
// send text msgs
|
// send text msgs
|
||||||
for await (const msg of msgs) {
|
for await (const msg of msgs) {
|
||||||
const { type: content_type, content, properties = {} } = msg;
|
const { type: content_type, content, properties = {} } = msg;
|
||||||
|
if ((content as string).trim() === '') continue; // 空消息不发送
|
||||||
properties.local_id = properties.local_id ?? +new Date();
|
properties.local_id = properties.local_id ?? +new Date();
|
||||||
await sendMessage({
|
await sendMessage({
|
||||||
id,
|
id,
|
||||||
@@ -94,7 +100,7 @@ const Send: FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// send files
|
// send files
|
||||||
if (uploadFiles && uploadFiles.length !== 0) {
|
if (uploadFiles.length !== 0) {
|
||||||
uploadFiles.forEach((fileInfo) => {
|
uploadFiles.forEach((fileInfo) => {
|
||||||
const ts = +new Date();
|
const ts = +new Date();
|
||||||
const { url, name, size, type } = fileInfo;
|
const { url, name, size, type } = fileInfo;
|
||||||
@@ -138,6 +144,12 @@ const Send: FC<IProps> = ({
|
|||||||
const members =
|
const members =
|
||||||
context == "channel" ? (channelsData[id]?.is_public ? uids : channelsData[id]?.members) : [];
|
context == "channel" ? (channelsData[id]?.is_public ? uids : channelsData[id]?.members) : [];
|
||||||
const isMarkdownMode = mode == Modes.markdown;
|
const isMarkdownMode = mode == Modes.markdown;
|
||||||
|
if (context == "user" && blocked) {
|
||||||
|
return <div className="p-5 bg-gray-200 rounded-lg w-full dark:bg-gray-600 text-red-300">
|
||||||
|
{t("contact_block_tip")}
|
||||||
|
<StyledButton className="mini ml-4" onClick={unblockThisContact}>{t("unblock")}</StyledButton>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* mobile input */}
|
{/* mobile input */}
|
||||||
@@ -154,6 +166,7 @@ const Send: FC<IProps> = ({
|
|||||||
<EmojiPicker selectEmoji={insertEmoji} />
|
<EmojiPicker selectEmoji={insertEmoji} />
|
||||||
{mode == Modes.text && (
|
{mode == Modes.text && (
|
||||||
<MixedInput
|
<MixedInput
|
||||||
|
updateMessages={setMsgs}
|
||||||
updateDraft={getUpdateDraft()}
|
updateDraft={getUpdateDraft()}
|
||||||
initialValue={getDraft()}
|
initialValue={getDraft()}
|
||||||
members={members}
|
members={members}
|
||||||
@@ -163,6 +176,8 @@ const Send: FC<IProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Toolbar
|
<Toolbar
|
||||||
|
sendMessages={handleSendMessage}
|
||||||
|
sendVisible={msgs.length > 0 || uploadFiles.length > 0}
|
||||||
context={context}
|
context={context}
|
||||||
to={id}
|
to={id}
|
||||||
mode={mode}
|
mode={mode}
|
||||||
|
|||||||
@@ -15,11 +15,16 @@ interface Props {
|
|||||||
|
|
||||||
const UserContextMenu: FC<Props> = ({ enable = false, uid, cid, visible, hide, children }) => {
|
const UserContextMenu: FC<Props> = ({ enable = false, uid, cid, visible, hide, children }) => {
|
||||||
const { t } = useTranslation("member");
|
const { t } = useTranslation("member");
|
||||||
|
const { t: chatTran } = useTranslation("chat");
|
||||||
const {
|
const {
|
||||||
|
blockThisContact,
|
||||||
|
removeFromContact,
|
||||||
copyEmail,
|
copyEmail,
|
||||||
canCopyEmail,
|
canCopyEmail,
|
||||||
startChat,
|
startChat,
|
||||||
canRemove,
|
canRemove,
|
||||||
|
canRemoveFromContact,
|
||||||
|
canBlock,
|
||||||
canRemoveFromChannel,
|
canRemoveFromChannel,
|
||||||
removeFromChannel,
|
removeFromChannel,
|
||||||
removeUser
|
removeUser
|
||||||
@@ -55,11 +60,22 @@ const UserContextMenu: FC<Props> = ({ enable = false, uid, cid, visible, hide, c
|
|||||||
title: t("remove_from_channel"),
|
title: t("remove_from_channel"),
|
||||||
handler: removeFromChannel
|
handler: removeFromChannel
|
||||||
},
|
},
|
||||||
|
canRemoveFromContact && {
|
||||||
|
danger: true,
|
||||||
|
title: t("remove_from_contact"),
|
||||||
|
handler: removeFromContact
|
||||||
|
},
|
||||||
|
canBlock && {
|
||||||
|
danger: true,
|
||||||
|
title: chatTran("block"),
|
||||||
|
handler: blockThisContact
|
||||||
|
},
|
||||||
canRemove && {
|
canRemove && {
|
||||||
danger: true,
|
danger: true,
|
||||||
title: t("remove"),
|
title: t("remove"),
|
||||||
handler: removeUser
|
handler: removeUser
|
||||||
}
|
}
|
||||||
|
|
||||||
].filter(Boolean) as Item[]
|
].filter(Boolean) as Item[]
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const UsersModal: FC<Props> = ({ closeModal }) => {
|
|||||||
<Modal>
|
<Modal>
|
||||||
<div className="flex flex-col w-80 md:w-[440px] max-h-[402px] bg-white dark:bg-gray-900 drop-shadow rounded-lg" ref={wrapperRef}>
|
<div className="flex flex-col w-80 md:w-[440px] max-h-[402px] bg-white dark:bg-gray-900 drop-shadow rounded-lg" ref={wrapperRef}>
|
||||||
<div className="shadow-md p-2">
|
<div className="shadow-md p-2">
|
||||||
<input className="p-2 text-sm bg-transparent dark:text-white" value={input} onChange={handleSearch} placeholder={t("search_user_placeholder")} />
|
<input className="p-2 text-sm bg-transparent dark:text-white w-full outline-none" value={input} onChange={handleSearch} placeholder={t("search_user_placeholder")} />
|
||||||
</div>
|
</div>
|
||||||
{users && (
|
{users && (
|
||||||
<ul className="flex flex-col overflow-y-scroll h-[260px] py-4">
|
<ul className="flex flex-col overflow-y-scroll h-[260px] py-4">
|
||||||
|
|||||||
@@ -23,11 +23,6 @@ const Version: FC<Props> = () => {
|
|||||||
<li>{t("client_version")}: {process.env.VERSION}</li>
|
<li>{t("client_version")}: {process.env.VERSION}</li>
|
||||||
<li>{t("server_version")}: {serverVersion}</li>
|
<li>{t("server_version")}: {serverVersion}</li>
|
||||||
<li>{t("build_time")}: {ts} <span className="text-gray-700 dark:text-gray-300">({dayjs(ts * 1000).fromNow()})</span></li>
|
<li>{t("build_time")}: {ts} <span className="text-gray-700 dark:text-gray-300">({dayjs(ts * 1000).fromNow()})</span></li>
|
||||||
<li>GitHub:
|
|
||||||
<strong className="font-bold">
|
|
||||||
<a className="text-primary-600 underline underline-offset-2" href="https://github.com/Privoce/vocechat-web/issues" target="_blank" rel="noopener noreferrer">vocechat-web/issues</a>
|
|
||||||
</strong>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<Button disabled={syncing} onClick={handleSync}> {syncing ? <Ring size={18} color="#fff" /> : t("sync")}</Button>
|
<Button disabled={syncing} onClick={handleSync}> {syncing ? <Ring size={18} color="#fff" /> : t("sync")}</Button>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const VoiceMessage = ({ file_path }: { file_path: string }) => {
|
|||||||
hideScrollbar: true,
|
hideScrollbar: true,
|
||||||
// mediaControls: true,
|
// mediaControls: true,
|
||||||
normalize: true,
|
normalize: true,
|
||||||
|
responsive: true
|
||||||
});
|
});
|
||||||
wave.load(audioSrc);
|
wave.load(audioSrc);
|
||||||
|
|
||||||
@@ -64,6 +65,12 @@ const VoiceMessage = ({ file_path }: { file_path: string }) => {
|
|||||||
const durDisplay = dayjs.duration(num, 'seconds').format('mm:ss');
|
const durDisplay = dayjs.duration(num, 'seconds').format('mm:ss');
|
||||||
setDuration(durDisplay);
|
setDuration(durDisplay);
|
||||||
}
|
}
|
||||||
|
// tricky
|
||||||
|
if (containerRef.current) {
|
||||||
|
const container = containerRef.current as HTMLDivElement;
|
||||||
|
const w = (container.querySelector("wave>canvas") as HTMLCanvasElement)?.width;
|
||||||
|
container.style.width = `${w}px`;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
VoiceMap[file_path] = wave;
|
VoiceMap[file_path] = wave;
|
||||||
};
|
};
|
||||||
@@ -103,11 +110,11 @@ const VoiceMessage = ({ file_path }: { file_path: string }) => {
|
|||||||
};
|
};
|
||||||
const notReady = status !== "ready";
|
const notReady = status !== "ready";
|
||||||
return (
|
return (
|
||||||
<div className={clsx("relative select-none flex items-center gap-2 p-2 rounded-lg max-w-sm", status === "error" ? "bg-red-200" : "bg-primary-100 dark:bg-primary-900")}>
|
<div className={clsx("relative whitespace-nowrap select-none flex items-center gap-2 p-2 rounded-lg max-w-sm", status === "error" ? "bg-red-200" : "bg-primary-100 dark:bg-primary-900")}>
|
||||||
<button className='disabled:opacity-60' onClick={handleClick} disabled={notReady}>
|
<button className='disabled:opacity-60' onClick={handleClick} disabled={notReady}>
|
||||||
{playing ? <IconPause className="stroke-primary-500" /> : <IconPlay className="stroke-primary-500" />}
|
{playing ? <IconPause className="stroke-primary-500" /> : <IconPlay className="stroke-primary-500" />}
|
||||||
</button>
|
</button>
|
||||||
<div ref={containerRef} className={clsx('flex-1 h-8', notReady && "flex-center flex-1 whitespace-nowrap")} >
|
<div ref={containerRef} className={clsx('flex-1 h-8 min-w-[100px]')} >
|
||||||
{status == "loading" && <span className='text-xs'>Loading voice message...</span>}
|
{status == "loading" && <span className='text-xs'>Loading voice message...</span>}
|
||||||
{status == "error" && <span className='text-xs text-red-800'>Load voice message error</span>}
|
{status == "error" && <span className='text-xs text-red-800'>Load voice message error</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useState, FC, DetailedHTMLProps, InputHTMLAttributes } from "react";
|
import { useState, FC, DetailedHTMLProps, InputHTMLAttributes, ReactElement } from "react";
|
||||||
import IconEyeOpen from "../../../assets/icons/eye.open.svg";
|
import IconEyeOpen from "../../../assets/icons/eye.open.svg";
|
||||||
import IconEyeClose from "../../../assets/icons/eye.close.svg";
|
import IconEyeClose from "../../../assets/icons/eye.close.svg";
|
||||||
interface Props
|
interface Props
|
||||||
@@ -24,7 +24,7 @@ interface Props
|
|||||||
>,
|
>,
|
||||||
HTMLInputElement
|
HTMLInputElement
|
||||||
> {
|
> {
|
||||||
prefix?: string;
|
prefix?: string | ReactElement;
|
||||||
ref?: any;
|
ref?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ const Input: FC<Props> = ({ type = "text", prefix = "", className = "", ...rest
|
|||||||
placeholder:text-gray-400`,
|
placeholder:text-gray-400`,
|
||||||
// noInner && 'rounded border border-solid border-gray-200 shadow',
|
// noInner && 'rounded border border-solid border-gray-200 shadow',
|
||||||
isLarge && 'py-3',
|
isLarge && 'py-3',
|
||||||
isNone && "border-none bg-transparent shadow-none",
|
isNone && "!border-none bg-transparent shadow-none",
|
||||||
isPwd && "pr-[30px]"
|
isPwd && "pr-[30px]"
|
||||||
);
|
);
|
||||||
return type == "password" ? (
|
return type == "password" ? (
|
||||||
@@ -59,7 +59,7 @@ const Input: FC<Props> = ({ type = "text", prefix = "", className = "", ...rest
|
|||||||
) : prefix ? (
|
) : prefix ? (
|
||||||
<div className={`w-full relative flex overflow-hidden rounded border border-solid border-gray-200 dark:border-gray-400 shadow-sm ${className}`}>
|
<div className={`w-full relative flex overflow-hidden rounded border border-solid border-gray-200 dark:border-gray-400 shadow-sm ${className}`}>
|
||||||
{typeof prefix === "string" ? <span className="px-4 py-2 text-sm text-gray-500 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 shadow-[rgb(0_0_0_/_10%)_-1px_0px_0px_inset]">{prefix}</span> : <span className="flex-center p-2 bg-gray-100 dark:bg-gray-800 ">{prefix}</span>}
|
{typeof prefix === "string" ? <span className="px-4 py-2 text-sm text-gray-500 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 shadow-[rgb(0_0_0_/_10%)_-1px_0px_0px_inset]">{prefix}</span> : <span className="flex-center p-2 bg-gray-100 dark:bg-gray-800 ">{prefix}</span>}
|
||||||
<input className={`${inputClass} ${className}`} type={inputType} {...rest} />
|
<input className={`${inputClass} ${className}`} type={type} {...rest} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<input type={inputType} className={`${inputClass} rounded border border-solid border-gray-200 dark:border-gray-400 shadow-sm ${className}`} {...rest} />
|
<input type={inputType} className={`${inputClass} rounded border border-solid border-gray-200 dark:border-gray-400 shadow-sm ${className}`} {...rest} />
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { StoredUser } from "../../app/slices/users";
|
import { StoredUser } from "../../app/slices/users";
|
||||||
import { useAppSelector } from "../../app/store";
|
import { useAppSelector } from "../../app/store";
|
||||||
|
import { compareVersion } from "../utils";
|
||||||
|
|
||||||
export default function useFilteredUsers() {
|
export default function useFilteredUsers() {
|
||||||
const [input, setInput] = useState("");
|
const [input, setInput] = useState("");
|
||||||
const users = useAppSelector((store) => Object.values(store.users.byId));
|
const { originUsers, serverVersion } = useAppSelector((store) => {
|
||||||
|
return {
|
||||||
|
originUsers: Object.values(store.users.byId),
|
||||||
|
serverVersion: store.server.version
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const enableContact = compareVersion(serverVersion, "0.3.7") >= 0;
|
||||||
const [filteredUsers, setFilteredUsers] = useState<StoredUser[]>([]);
|
const [filteredUsers, setFilteredUsers] = useState<StoredUser[]>([]);
|
||||||
|
const users = enableContact ? originUsers.filter((u) => u.status == "added") : originUsers;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!input) {
|
if (!input) {
|
||||||
setFilteredUsers(users);
|
setFilteredUsers(users);
|
||||||
@@ -19,7 +27,7 @@ export default function useFilteredUsers() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [input]);
|
}, [input, users.length]);
|
||||||
|
|
||||||
const updateInput = (val: string) => {
|
const updateInput = (val: string) => {
|
||||||
setInput(val);
|
setInput(val);
|
||||||
|
|||||||
@@ -1,35 +1,42 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import useCopy from "./useCopy";
|
import useCopy from "./useCopy";
|
||||||
import { useGetSMTPStatusQuery } from "../../app/services/server";
|
import { useGetSMTPStatusQuery } from "../../app/services/server";
|
||||||
import { useLazyCreateInviteLinkQuery as useCreateChannelInviteLinkQuery } from "../../app/services/channel";
|
import { useLazyCreateInviteLinkQuery as useCreateChannelInviteLinkQuery, useLazyCreatePrivateInviteLinkQuery } from "../../app/services/channel";
|
||||||
|
import { useAppSelector } from "../../app/store";
|
||||||
|
|
||||||
export default function useInviteLink(cid?: number) {
|
export default function useInviteLink(cid?: number) {
|
||||||
const [finalLink, setFinalLink] = useState("");
|
const [finalLink, setFinalLink] = useState("");
|
||||||
|
const channel = useAppSelector(store => store.channels.byId[cid ?? 0]);
|
||||||
const { data: SMTPEnabled, isSuccess: smtpStatusFetchSuccess } = useGetSMTPStatusQuery();
|
const { data: SMTPEnabled, isSuccess: smtpStatusFetchSuccess } = useGetSMTPStatusQuery();
|
||||||
const [generateChannelInviteLink, { data: channelInviteLink, isLoading: generatingChannelLink }] =
|
const [generateChannelInviteLink, { data: channelInviteLink, isLoading: generatingChannelLink }] =
|
||||||
useCreateChannelInviteLinkQuery();
|
useCreateChannelInviteLinkQuery();
|
||||||
|
const [generatePrivateInviteLink, { data: privateInviteLink, isLoading: generatingPrivateLink }] = useLazyCreatePrivateInviteLinkQuery();
|
||||||
|
|
||||||
const { copied, copy } = useCopy({ enableToast: false });
|
const { copied, copy } = useCopy({ enableToast: false });
|
||||||
const copyLink = () => {
|
const copyLink = () => {
|
||||||
copy(finalLink);
|
copy(finalLink);
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
generateChannelInviteLink(cid);
|
if (!cid || channel?.is_public) {
|
||||||
}, [cid]);
|
generateChannelInviteLink(cid);
|
||||||
|
} else {
|
||||||
|
generatePrivateInviteLink(cid);
|
||||||
|
}
|
||||||
|
}, [cid, channel]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const _link = channelInviteLink;
|
const _link = channelInviteLink || privateInviteLink;
|
||||||
if (_link && smtpStatusFetchSuccess) {
|
if (_link && smtpStatusFetchSuccess) {
|
||||||
setFinalLink(_link);
|
setFinalLink(_link);
|
||||||
}
|
}
|
||||||
}, [channelInviteLink, smtpStatusFetchSuccess]);
|
}, [channelInviteLink, privateInviteLink, smtpStatusFetchSuccess]);
|
||||||
const genServerLink = () => {
|
const genServerLink = () => {
|
||||||
generateChannelInviteLink();
|
generateChannelInviteLink();
|
||||||
};
|
};
|
||||||
|
const generating = generatingPrivateLink || generatingChannelLink;
|
||||||
return {
|
return {
|
||||||
enableSMTP: SMTPEnabled,
|
enableSMTP: SMTPEnabled,
|
||||||
generating: generatingChannelLink,
|
generating,
|
||||||
generateNewLink: cid ? generateChannelInviteLink.bind(null, cid) : genServerLink,
|
generateNewLink: cid ? generateChannelInviteLink.bind(null, cid) : genServerLink,
|
||||||
link: finalLink,
|
link: finalLink,
|
||||||
linkCopied: copied,
|
linkCopied: copied,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect } from "react";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import initCache, { useRehydrate } from "../../app/cache";
|
import initCache, { useRehydrate } from "../../app/cache";
|
||||||
import { useLazyGetFavoritesQuery } from "../../app/services/message";
|
import { useLazyGetFavoritesQuery } from "../../app/services/message";
|
||||||
import { useLazyGetUsersQuery } from "../../app/services/user";
|
import { useLazyGetContactsQuery, useLazyGetUsersQuery } from "../../app/services/user";
|
||||||
import { useLazyGetSystemCommonQuery, useLazyGetServerQuery, useLazyGetServerVersionQuery } from "../../app/services/server";
|
import { useLazyGetSystemCommonQuery, useLazyGetServerQuery, useLazyGetServerVersionQuery } from "../../app/services/server";
|
||||||
import useStreaming from "./useStreaming";
|
import useStreaming from "./useStreaming";
|
||||||
import { useAppSelector } from "../../app/store";
|
import { useAppSelector } from "../../app/store";
|
||||||
@@ -44,19 +44,21 @@ export default function usePreload() {
|
|||||||
data: favorites
|
data: favorites
|
||||||
}
|
}
|
||||||
] = useLazyGetFavoritesQuery();
|
] = useLazyGetFavoritesQuery();
|
||||||
|
const [getUsers, { isLoading: usersLoading, isSuccess: usersSuccess, isError: usersError, data: users }] = useLazyGetUsersQuery();
|
||||||
const [
|
const [
|
||||||
getUsers,
|
getContacts,
|
||||||
{ isLoading: usersLoading, isSuccess: usersSuccess, isError: usersError, data: users }
|
{ isLoading: contactsLoading, isSuccess: contactsSuccess, isError: contactsError, data: contacts }
|
||||||
] = useLazyGetUsersQuery();
|
] = useLazyGetContactsQuery();
|
||||||
const [
|
const [
|
||||||
getServer,
|
getServer,
|
||||||
{ isLoading: serverLoading, isSuccess: serverSuccess, isError: serverError, data: server }
|
{ isLoading: serverLoading, isSuccess: serverSuccess, isError: serverError, data: server }
|
||||||
] = useLazyGetServerQuery();
|
] = useLazyGetServerQuery();
|
||||||
const [getServerVersion, { isSuccess: serverVersionSuccess, isLoading: loadingServerVersion }] = useLazyGetServerVersionQuery();
|
const [getServerVersion, { data: serverVersion, isSuccess: serverVersionSuccess, isLoading: loadingServerVersion }] = useLazyGetServerVersionQuery();
|
||||||
const [getSystemCommon] = useLazyGetSystemCommonQuery();
|
const [getSystemCommon] = useLazyGetSystemCommonQuery();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initCache();
|
initCache();
|
||||||
rehydrate();
|
rehydrate();
|
||||||
|
getServerVersion();
|
||||||
return () => {
|
return () => {
|
||||||
setStreamingReady(false);
|
setStreamingReady(false);
|
||||||
};
|
};
|
||||||
@@ -75,18 +77,21 @@ export default function usePreload() {
|
|||||||
}
|
}
|
||||||
}, [channelIds, channelMessageData, isGuest]);
|
}, [channelIds, channelMessageData, isGuest]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rehydrated) {
|
if (rehydrated && serverVersion) {
|
||||||
getUsers();
|
getUsers().then(() => {
|
||||||
getServer();
|
if (compareVersion(serverVersion, '0.3.7') >= 0) {
|
||||||
getFavorites();
|
// 根据版本号判断是否需要初始化contact
|
||||||
getServerVersion().then((resp) => {
|
getContacts();
|
||||||
if (resp.data && compareVersion(resp.data, '0.3.4') >= 0) {
|
|
||||||
// 根据版本号判断是否需要调用system common api
|
|
||||||
getSystemCommon();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
getServer();
|
||||||
|
getFavorites();
|
||||||
|
if (compareVersion(serverVersion, '0.3.4') >= 0) {
|
||||||
|
// 根据版本号判断是否需要调用system common api
|
||||||
|
getSystemCommon();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [rehydrated]);
|
}, [rehydrated, serverVersion]);
|
||||||
const tokenAlmostExpire = dayjs().isAfter(new Date(expireTime - 20 * 1000));
|
const tokenAlmostExpire = dayjs().isAfter(new Date(expireTime - 20 * 1000));
|
||||||
const canStreaming = !!loginUid && rehydrated && !!token && !tokenAlmostExpire;
|
const canStreaming = !!loginUid && rehydrated && !!token && !tokenAlmostExpire;
|
||||||
// console.log("ttt", loginUid, rehydrated, token);
|
// console.log("ttt", loginUid, rehydrated, token);
|
||||||
@@ -94,13 +99,13 @@ export default function usePreload() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setStreamingReady(canStreaming);
|
setStreamingReady(canStreaming);
|
||||||
}, [canStreaming]);
|
}, [canStreaming]);
|
||||||
|
const enableContact = serverVersion ? compareVersion(serverVersion, '0.3.7') >= 0 : false;
|
||||||
return {
|
return {
|
||||||
loading: usersLoading || serverLoading || favoritesLoading || !rehydrated || loadingLicense || loadingServerVersion,
|
loading: usersLoading || serverLoading || favoritesLoading || !rehydrated || loadingLicense || loadingServerVersion,
|
||||||
error: usersError && serverError && favoritesError,
|
error: usersError && serverError && favoritesError,
|
||||||
success: usersSuccess && serverSuccess && favoritesSuccess && serverVersionSuccess,
|
success: usersSuccess && serverSuccess && favoritesSuccess && serverVersionSuccess,
|
||||||
data: {
|
data: {
|
||||||
users,
|
users: enableContact ? contacts : users,
|
||||||
server,
|
server,
|
||||||
favorites
|
favorites
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,15 @@ import {
|
|||||||
updateReadChannels,
|
updateReadChannels,
|
||||||
updateReadUsers,
|
updateReadUsers,
|
||||||
updateMute,
|
updateMute,
|
||||||
updateAutoDeleteSetting
|
updateAutoDeleteSetting,
|
||||||
|
upsertPinChats,
|
||||||
|
removePinChats
|
||||||
} from "../../../app/slices/footprint";
|
} from "../../../app/slices/footprint";
|
||||||
import { updateUsersByLogs, updateUsersStatus } from "../../../app/slices/users";
|
import { updateContactStatus, updateUsersByLogs, updateUsersStatus } from "../../../app/slices/users";
|
||||||
import { resetAuthData, updateLoginUser, updateRoleChanged } from "../../../app/slices/auth.data";
|
import { resetAuthData, updateLoginUser, updateRoleChanged } from "../../../app/slices/auth.data";
|
||||||
import chatMessageHandler from "./chat.handler";
|
import chatMessageHandler from "./chat.handler";
|
||||||
import { useAppDispatch, useAppSelector } from "../../../app/store";
|
import { useAppDispatch, useAppSelector } from "../../../app/store";
|
||||||
import { ServerEvent, UsersStateEvent } from "../../../types/sse";
|
import { ServerEvent, UserSettingsChangedEvent, UsersStateEvent } from "../../../types/sse";
|
||||||
import { useRenewMutation } from "../../../app/services/auth";
|
import { useRenewMutation } from "../../../app/services/auth";
|
||||||
import { getLocalAuthData } from "../../utils";
|
import { getLocalAuthData } from "../../utils";
|
||||||
import { removeUserSession } from "../../../app/slices/message.user";
|
import { removeUserSession } from "../../../app/slices/message.user";
|
||||||
@@ -149,6 +151,11 @@ export default function useStreaming() {
|
|||||||
dispatch(clearChannelMessage(gid));
|
dispatch(clearChannelMessage(gid));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "pinned_chats": {
|
||||||
|
const { chats } = data;
|
||||||
|
dispatch(upsertPinChats({ pins: chats, override: true }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "users_snapshot": {
|
case "users_snapshot": {
|
||||||
const { version } = data;
|
const { version } = data;
|
||||||
dispatch(updateUsersVersion(version));
|
dispatch(updateUsersVersion(version));
|
||||||
@@ -187,6 +194,20 @@ export default function useStreaming() {
|
|||||||
case "read_index_users":
|
case "read_index_users":
|
||||||
dispatch(updateReadUsers(data[key]));
|
dispatch(updateReadUsers(data[key]));
|
||||||
break;
|
break;
|
||||||
|
case "add_pin_chats": {
|
||||||
|
const pins = (data as UserSettingsChangedEvent).add_pin_chats ?? [];
|
||||||
|
if (pins.length) {
|
||||||
|
dispatch(upsertPinChats({ pins }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "remove_pin_chats": {
|
||||||
|
const pins = (data as UserSettingsChangedEvent).remove_pin_chats ?? [];
|
||||||
|
if (pins.length) {
|
||||||
|
dispatch(removePinChats(pins));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "add_mute_users":
|
case "add_mute_users":
|
||||||
case "mute_users":
|
case "mute_users":
|
||||||
case "add_mute_groups":
|
case "add_mute_groups":
|
||||||
@@ -199,6 +220,31 @@ export default function useStreaming() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case "remove_contacts": {
|
||||||
|
const arr = (data as UserSettingsChangedEvent).remove_contacts ?? [];
|
||||||
|
if (arr.length) {
|
||||||
|
dispatch(updateContactStatus(
|
||||||
|
arr.map(
|
||||||
|
(uid: number) => {
|
||||||
|
return { uid, status: "" };
|
||||||
|
}
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "add_contacts": {
|
||||||
|
const arr = (data as UserSettingsChangedEvent).add_contacts ?? [];
|
||||||
|
if (arr.length) {
|
||||||
|
const transformed = arr.map(
|
||||||
|
({ target_uid, info: { status } }) => {
|
||||||
|
return { uid: target_uid, status };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
dispatch(updateContactStatus(transformed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
case "remove_mute_users":
|
case "remove_mute_users":
|
||||||
case "remove_mute_groups":
|
case "remove_mute_groups":
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import toast from "react-hot-toast";
|
|||||||
import { useNavigate, useMatch } from "react-router-dom";
|
import { useNavigate, useMatch } from "react-router-dom";
|
||||||
import { hideAll } from "tippy.js";
|
import { hideAll } from "tippy.js";
|
||||||
import { useRemoveMembersMutation } from "../../app/services/channel";
|
import { useRemoveMembersMutation } from "../../app/services/channel";
|
||||||
import { useLazyDeleteUserQuery } from "../../app/services/user";
|
import { useLazyDeleteUserQuery, useUpdateContactStatusMutation } from "../../app/services/user";
|
||||||
// import useConfig from "./useConfig";
|
// import useConfig from "./useConfig";
|
||||||
import useCopy from "./useCopy";
|
import useCopy from "./useCopy";
|
||||||
import { useAppSelector } from "../../app/store";
|
import { useAppSelector } from "../../app/store";
|
||||||
@@ -18,6 +18,7 @@ const useUserOperation = ({ uid, cid }: IProps) => {
|
|||||||
const { t: ct } = useTranslation();
|
const { t: ct } = useTranslation();
|
||||||
const [passedUid, setPassedUid] = useState<number | undefined>(undefined);
|
const [passedUid, setPassedUid] = useState<number | undefined>(undefined);
|
||||||
const isUserDetailPath = useMatch(`/users/${uid}`);
|
const isUserDetailPath = useMatch(`/users/${uid}`);
|
||||||
|
const [updateContactStatus] = useUpdateContactStatusMutation();
|
||||||
const [removeUser, { isSuccess: removeUserSuccess }] = useLazyDeleteUserQuery();
|
const [removeUser, { isSuccess: removeUserSuccess }] = useLazyDeleteUserQuery();
|
||||||
const [removeInChannel, { isSuccess: removeSuccess }] = useRemoveMembersMutation();
|
const [removeInChannel, { isSuccess: removeSuccess }] = useRemoveMembersMutation();
|
||||||
const navigateTo = useNavigate();
|
const navigateTo = useNavigate();
|
||||||
@@ -71,6 +72,19 @@ const useUserOperation = ({ uid, cid }: IProps) => {
|
|||||||
const startChat = () => {
|
const startChat = () => {
|
||||||
navigateTo(`/chat/dm/${uid}`);
|
navigateTo(`/chat/dm/${uid}`);
|
||||||
};
|
};
|
||||||
|
const removeFromContact = () => {
|
||||||
|
if (uid) {
|
||||||
|
updateContactStatus({ target_uid: uid, action: "remove" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const blockThisContact = () => {
|
||||||
|
if (uid === undefined) return;
|
||||||
|
updateContactStatus({ target_uid: uid, action: "block" });
|
||||||
|
};
|
||||||
|
const unblockThisContact = () => {
|
||||||
|
if (uid === undefined) return;
|
||||||
|
updateContactStatus({ target_uid: uid, action: "unblock" });
|
||||||
|
};
|
||||||
|
|
||||||
const isAdmin = !!loginUser?.is_admin;
|
const isAdmin = !!loginUser?.is_admin;
|
||||||
const loginUid = loginUser?.uid;
|
const loginUid = loginUser?.uid;
|
||||||
@@ -81,8 +95,16 @@ const useUserOperation = ({ uid, cid }: IProps) => {
|
|||||||
(isAdmin || channel?.owner == loginUid) &&
|
(isAdmin || channel?.owner == loginUid) &&
|
||||||
uid != channel?.owner;
|
uid != channel?.owner;
|
||||||
const canRemove: boolean = isAdmin && loginUid != uid && !cid && uid !== 1;
|
const canRemove: boolean = isAdmin && loginUid != uid && !cid && uid !== 1;
|
||||||
|
const canBlock: boolean = loginUid != uid;
|
||||||
|
const canRemoveFromContact: boolean = loginUid != uid;
|
||||||
const canInviteChannel = !!cid && (loginUser?.is_admin || channel?.owner == loginUser?.uid);
|
const canInviteChannel = !!cid && (loginUser?.is_admin || channel?.owner == loginUser?.uid);
|
||||||
return {
|
return {
|
||||||
|
removeFromContact,
|
||||||
|
canBlock,
|
||||||
|
canRemoveFromContact,
|
||||||
|
blocked: user?.status == "blocked",
|
||||||
|
blockThisContact,
|
||||||
|
unblockThisContact,
|
||||||
canInviteChannel,
|
canInviteChannel,
|
||||||
canDeleteChannel,
|
canDeleteChannel,
|
||||||
canRemove,
|
canRemove,
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
// import React from 'react';
|
||||||
|
import IconAdd from '../../../assets/icons/add.person.svg';
|
||||||
|
import IconBlock from '../../../assets/icons/block.svg';
|
||||||
|
import { useAppSelector } from '../../../app/store';
|
||||||
|
import { useUpdateContactStatusMutation } from '../../../app/services/user';
|
||||||
|
import { ContactAction } from '../../../types/user';
|
||||||
|
import ServerVersionChecker from '../../../common/component/ServerVersionChecker';
|
||||||
|
// import toast from 'react-hot-toast';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uid: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddContactTip = (props: Props) => {
|
||||||
|
const { t } = useTranslation("chat");
|
||||||
|
const [updateContactStatus] = useUpdateContactStatusMutation();
|
||||||
|
const targetUser = useAppSelector(store => store.users.byId[props.uid]);
|
||||||
|
const handleContactStatus = (action: ContactAction) => {
|
||||||
|
updateContactStatus({ target_uid: props.uid, action });
|
||||||
|
};
|
||||||
|
// useEffect(() => {
|
||||||
|
// if(isSuccess){
|
||||||
|
// toast.success(t("tip"))
|
||||||
|
// }
|
||||||
|
// }, [isSuccess])
|
||||||
|
|
||||||
|
const itemClass = `cursor-pointer flex flex-col items-center gap-1 rounded-lg w-32 text-primary-400 bg-gray-50 dark:bg-gray-800 text-sm pt-3.5 pb-3`;
|
||||||
|
if (!targetUser) return null;
|
||||||
|
if (targetUser.status == "added") return null;
|
||||||
|
const blocked = targetUser.status == "blocked";
|
||||||
|
return (
|
||||||
|
<ServerVersionChecker version='0.3.7' empty={true}>
|
||||||
|
<div className="py-4 px-10 flex flex-col items-center gap-3 bg-slate-100 dark:bg-slate-600">
|
||||||
|
<h3 className='text-gray-700 dark:text-gray-300 text-sm font-semibold'>{blocked ? t("contact_block_tip") : t("contact_tip")}</h3>
|
||||||
|
<ul className='flex gap-4'>
|
||||||
|
{!blocked && <li className={itemClass} onClick={handleContactStatus.bind(null, "add")}>
|
||||||
|
<IconAdd className="fill-primary-400" />
|
||||||
|
<span>{t("add_contact")}</span>
|
||||||
|
</li>}
|
||||||
|
<li className={itemClass} onClick={blocked ? handleContactStatus.bind(null, "unblock") : handleContactStatus.bind(null, "block")}>
|
||||||
|
<IconBlock className="stroke-primary-400" />
|
||||||
|
<span>{blocked ? t("unblock") : t("block")}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</ServerVersionChecker>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddContactTip;
|
||||||
@@ -18,6 +18,7 @@ import ImagePreview from "../../../common/component/ImagePreview";
|
|||||||
|
|
||||||
import VirtualMessageFeed from "./VirtualMessageFeed";
|
import VirtualMessageFeed from "./VirtualMessageFeed";
|
||||||
import DMVoice from "./DMVoicing";
|
import DMVoice from "./DMVoicing";
|
||||||
|
import AddContactTip from "./AddContactTip";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
@@ -100,6 +101,7 @@ const Layout: FC<Props> = ({
|
|||||||
<div className="w-full h-full flex items-start justify-between relative" >
|
<div className="w-full h-full flex items-start justify-between relative" >
|
||||||
<div className="rounded-br-2xl flex flex-col absolute bottom-0 w-full h-full" ref={messagesContainer}>
|
<div className="rounded-br-2xl flex flex-col absolute bottom-0 w-full h-full" ref={messagesContainer}>
|
||||||
{context == "user" && <DMVoice uid={to} />}
|
{context == "user" && <DMVoice uid={to} />}
|
||||||
|
{context == "user" && <AddContactTip uid={to} />}
|
||||||
{/* 消息流 */}
|
{/* 消息流 */}
|
||||||
<VirtualMessageFeed key={`${context}_${to}`} context={context} id={to} />
|
<VirtualMessageFeed key={`${context}_${to}`} context={context} id={to} />
|
||||||
{/* 发送框 */}
|
{/* 发送框 */}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { FC, ReactElement } from "react";
|
|||||||
import Tippy from "@tippyjs/react";
|
import Tippy from "@tippyjs/react";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { useNavigate, useLocation, useMatch } from "react-router-dom";
|
import { useNavigate, useLocation, useMatch } from "react-router-dom";
|
||||||
import { useUpdateMuteSettingMutation } from "../../../app/services/user";
|
import { usePinChatMutation, useUnpinChatMutation, useUpdateMuteSettingMutation } from "../../../app/services/user";
|
||||||
import { useReadMessageMutation } from "../../../app/services/message";
|
import { useReadMessageMutation } from "../../../app/services/message";
|
||||||
import { removeUserSession } from "../../../app/slices/message.user";
|
import { removeUserSession } from "../../../app/slices/message.user";
|
||||||
import ContextMenu, { Item } from "../../../common/component/ContextMenu";
|
import ContextMenu, { Item } from "../../../common/component/ContextMenu";
|
||||||
@@ -10,8 +10,10 @@ import useUserOperation from "../../../common/hook/useUserOperation";
|
|||||||
import { useAppSelector } from "../../../app/store";
|
import { useAppSelector } from "../../../app/store";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ChatContext } from "../../../types/common";
|
import { ChatContext } from "../../../types/common";
|
||||||
|
import { compareVersion } from "../../../common/utils";
|
||||||
type Props = {
|
type Props = {
|
||||||
context: ChatContext;
|
context: ChatContext;
|
||||||
|
pinned: boolean;
|
||||||
id: number;
|
id: number;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
mid: number;
|
mid: number;
|
||||||
@@ -21,6 +23,7 @@ type Props = {
|
|||||||
children: ReactElement;
|
children: ReactElement;
|
||||||
};
|
};
|
||||||
const SessionContextMenu: FC<Props> = ({
|
const SessionContextMenu: FC<Props> = ({
|
||||||
|
pinned,
|
||||||
context = "dm",
|
context = "dm",
|
||||||
id,
|
id,
|
||||||
visible,
|
visible,
|
||||||
@@ -36,13 +39,16 @@ const SessionContextMenu: FC<Props> = ({
|
|||||||
cid: context == "channel" ? id : undefined
|
cid: context == "channel" ? id : undefined
|
||||||
});
|
});
|
||||||
const [muteChannel] = useUpdateMuteSettingMutation();
|
const [muteChannel] = useUpdateMuteSettingMutation();
|
||||||
|
const [pinChat] = usePinChatMutation();
|
||||||
|
const [unpinChat] = useUnpinChatMutation();
|
||||||
const [updateReadIndex] = useReadMessageMutation();
|
const [updateReadIndex] = useReadMessageMutation();
|
||||||
const pathMatched = useMatch(`/chat/dm/${id}`);
|
const pathMatched = useMatch(`/chat/dm/${id}`);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigateTo = useNavigate();
|
const navigateTo = useNavigate();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
const { channelMuted } = useAppSelector((store) => {
|
const { channelMuted, serverVersion } = useAppSelector((store) => {
|
||||||
return {
|
return {
|
||||||
|
serverVersion: store.server.version,
|
||||||
channelMuted: context == "channel" ? store.footprint.muteChannels[id] : false
|
channelMuted: context == "channel" ? store.footprint.muteChannels[id] : false
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -70,13 +76,27 @@ const SessionContextMenu: FC<Props> = ({
|
|||||||
const data = channelMuted ? { remove_groups: [id] } : { add_groups: [{ gid: id }] };
|
const data = channelMuted ? { remove_groups: [id] } : { add_groups: [{ gid: id }] };
|
||||||
muteChannel(data);
|
muteChannel(data);
|
||||||
};
|
};
|
||||||
|
const handlePinChat = () => {
|
||||||
|
const data = context == "dm" ? { uid: id } : { gid: id };
|
||||||
|
if (pinned) {
|
||||||
|
unpinChat(data);
|
||||||
|
} else {
|
||||||
|
pinChat(data);
|
||||||
|
}
|
||||||
|
};
|
||||||
const handleDMSetting = () => {
|
const handleDMSetting = () => {
|
||||||
navigateTo(`/setting/dm/${id}/overview?f=${pathname}`);
|
navigateTo(`/setting/dm/${id}/overview?f=${pathname}`);
|
||||||
};
|
};
|
||||||
|
const pinTxt = t(pinned ? "unpin_chat" : "pin_chat", { ns: "chat" });
|
||||||
|
|
||||||
|
const pinVisible = compareVersion(serverVersion, "0.3.7") >= 0;
|
||||||
const items =
|
const items =
|
||||||
context == "dm"
|
context == "dm"
|
||||||
? [
|
? [
|
||||||
|
pinVisible && {
|
||||||
|
title: pinTxt,
|
||||||
|
handler: handlePinChat
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("action.mark_read"),
|
title: t("action.mark_read"),
|
||||||
handler: handleReadAll
|
handler: handleReadAll
|
||||||
@@ -96,6 +116,10 @@ const SessionContextMenu: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
|
pinVisible && {
|
||||||
|
title: pinTxt,
|
||||||
|
handler: handlePinChat
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t("setting"),
|
title: t("setting"),
|
||||||
underline: true,
|
underline: true,
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ interface IProps {
|
|||||||
type?: ChatContext;
|
type?: ChatContext;
|
||||||
id: number;
|
id: number;
|
||||||
mid: number;
|
mid: number;
|
||||||
|
pinned?: boolean;
|
||||||
setDeleteChannelId: (param: number) => void;
|
setDeleteChannelId: (param: number) => void;
|
||||||
setInviteChannelId: (param: number) => void;
|
setInviteChannelId: (param: number) => void;
|
||||||
}
|
}
|
||||||
const Session: FC<IProps> = ({
|
const Session: FC<IProps> = ({
|
||||||
type = "dm",
|
type = "dm",
|
||||||
|
pinned = false,
|
||||||
id,
|
id,
|
||||||
mid,
|
mid,
|
||||||
setDeleteChannelId,
|
setDeleteChannelId,
|
||||||
@@ -85,7 +87,7 @@ const Session: FC<IProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tmp = type == "dm" ? userData[id] : channelData[id];
|
const tmp = type == "dm" ? userData[id] : channelData[id];
|
||||||
if (!tmp) return;
|
if (!tmp) return;
|
||||||
if ("avatar" in tmp) {
|
if (type == "dm") {
|
||||||
// user
|
// user
|
||||||
const { name, avatar } = tmp;
|
const { name, avatar } = tmp;
|
||||||
setData({ name, icon: avatar, mid, is_public: true });
|
setData({ name, icon: avatar, mid, is_public: true });
|
||||||
@@ -110,13 +112,14 @@ const Session: FC<IProps> = ({
|
|||||||
console.log("unreads", unreads, isCurrentPath);
|
console.log("unreads", unreads, isCurrentPath);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="session">
|
<li className={clsx("session")}>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
visible={contextMenuVisible}
|
visible={contextMenuVisible}
|
||||||
hide={hideContextMenu}
|
hide={hideContextMenu}
|
||||||
context={type}
|
context={type}
|
||||||
id={id}
|
id={id}
|
||||||
mid={mid}
|
mid={mid}
|
||||||
|
pinned={pinned}
|
||||||
setInviteChannelId={setInviteChannelId}
|
setInviteChannelId={setInviteChannelId}
|
||||||
deleteChannel={setDeleteChannelId}
|
deleteChannel={setDeleteChannelId}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import DeleteChannelConfirmModal from "../../settingChannel/DeleteConfirmModal";
|
|||||||
import InviteModal from "../../../common/component/InviteModal";
|
import InviteModal from "../../../common/component/InviteModal";
|
||||||
import { useAppSelector } from "../../../app/store";
|
import { useAppSelector } from "../../../app/store";
|
||||||
import { ChatContext } from "../../../types/common";
|
import { ChatContext } from "../../../types/common";
|
||||||
|
import { PinChatTargetChannel, PinChatTargetUser } from "../../../types/sse";
|
||||||
export interface ChatSession {
|
export interface ChatSession {
|
||||||
type: ChatContext;
|
type: ChatContext;
|
||||||
id: number;
|
id: number;
|
||||||
@@ -20,9 +21,11 @@ const SessionList: FC<Props> = ({ tempSession }) => {
|
|||||||
const [deleteId, setDeleteId] = useState<number>();
|
const [deleteId, setDeleteId] = useState<number>();
|
||||||
const [inviteChannelId, setInviteChannelId] = useState<number>();
|
const [inviteChannelId, setInviteChannelId] = useState<number>();
|
||||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||||
const { channelIDs, DMs, readChannels, readUsers, channelMessage, userMessage, loginUid } =
|
const [pinSessions, setPinSessions] = useState<ChatSession[]>([]);
|
||||||
|
const { pins, channelIDs, DMs, readChannels, readUsers, channelMessage, userMessage, loginUid } =
|
||||||
useAppSelector((store) => {
|
useAppSelector((store) => {
|
||||||
return {
|
return {
|
||||||
|
pins: store.footprint.pinChats,
|
||||||
loginUid: store.authData.user?.uid,
|
loginUid: store.authData.user?.uid,
|
||||||
channelIDs: store.channels.ids,
|
channelIDs: store.channels.ids,
|
||||||
DMs: store.userMessage.ids,
|
DMs: store.userMessage.ids,
|
||||||
@@ -34,24 +37,49 @@ const SessionList: FC<Props> = ({ tempSession }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cSessions = channelIDs.map((id) => {
|
// const pinDMs=
|
||||||
const mids = channelMessage[id];
|
const getSessionObj = (id: number, type: "dm" | "channel") => {
|
||||||
|
const mids = type == "dm" ? userMessage[id] : channelMessage[id];
|
||||||
if (!mids || mids.length == 0) {
|
if (!mids || mids.length == 0) {
|
||||||
return { unreads: 0, id, type: "channel" };
|
return { unread: 0, id, type } as ChatSession;
|
||||||
}
|
}
|
||||||
// 先转换成数字,再排序
|
// 先转换成数字,再排序
|
||||||
const mid = [...mids].sort((a, b) => +a - +b).pop();
|
const mid = [...mids].sort((a, b) => +a - +b).pop();
|
||||||
return { id, mid, type: "channel" };
|
return { id, mid, type } as ChatSession;
|
||||||
|
};
|
||||||
|
const pinTmps = pins.map((p) => {
|
||||||
|
const { target } = p;
|
||||||
|
if ("uid" in target) {
|
||||||
|
return getSessionObj((target as PinChatTargetUser).uid, "dm");
|
||||||
|
}
|
||||||
|
if ("gid" in target) {
|
||||||
|
return getSessionObj((target as PinChatTargetChannel).gid, "channel");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).filter((p) => !!p);
|
||||||
|
const channelPinIds = pins.map(p => {
|
||||||
|
if (p.target.gid) {
|
||||||
|
return p.target.gid;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}).filter(id => !!id);
|
||||||
|
const dmPinIds = pins.map(p => {
|
||||||
|
if (p.target.uid) {
|
||||||
|
return p.target.uid;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}).filter(id => !!id);
|
||||||
|
const cSessions = channelIDs.filter(id => {
|
||||||
|
return !channelPinIds.includes(id);
|
||||||
|
}).map((id) => {
|
||||||
|
return getSessionObj(id, "channel");
|
||||||
});
|
});
|
||||||
const uSessions = DMs.map((id) => {
|
const uSessions = DMs.filter(id => {
|
||||||
// console.log("adddd", id);
|
return !dmPinIds.includes(id);
|
||||||
const mids = userMessage[id];
|
}).map((id) => {
|
||||||
if (!mids || mids.length == 0) {
|
return getSessionObj(id, "dm");
|
||||||
return { unreads: 0, id, type: "dm" };
|
|
||||||
}
|
|
||||||
// 先转换成数字,再排序
|
|
||||||
const mid = [...mids].sort((a, b) => +a - +b).pop();
|
|
||||||
return { type: "dm", id, mid };
|
|
||||||
});
|
});
|
||||||
const temps = [...(cSessions as ChatSession[]), ...(uSessions as ChatSession[])].sort((a, b) => {
|
const temps = [...(cSessions as ChatSession[]), ...(uSessions as ChatSession[])].sort((a, b) => {
|
||||||
const { mid: aMid = 0 } = a;
|
const { mid: aMid = 0 } = a;
|
||||||
@@ -62,6 +90,7 @@ const SessionList: FC<Props> = ({ tempSession }) => {
|
|||||||
const newSessions = tempSession ? [tempSession, ...temps] : temps;
|
const newSessions = tempSession ? [tempSession, ...temps] : temps;
|
||||||
// console.log("qqqq", newSessions);
|
// console.log("qqqq", newSessions);
|
||||||
setSessions(newSessions);
|
setSessions(newSessions);
|
||||||
|
setPinSessions(pinTmps);
|
||||||
}, [
|
}, [
|
||||||
channelIDs,
|
channelIDs,
|
||||||
DMs,
|
DMs,
|
||||||
@@ -70,11 +99,29 @@ const SessionList: FC<Props> = ({ tempSession }) => {
|
|||||||
readUsers,
|
readUsers,
|
||||||
loginUid,
|
loginUid,
|
||||||
userMessage,
|
userMessage,
|
||||||
tempSession
|
tempSession,
|
||||||
|
pins
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{pinSessions.length ? <ul className="flex flex-col gap-0.5 p-2 bg-primary-500/10">
|
||||||
|
{pinSessions.map((p) => {
|
||||||
|
const { type, id, mid = 0 } = p;
|
||||||
|
const key = `${type}_${id}`;
|
||||||
|
return (
|
||||||
|
<Session
|
||||||
|
key={key}
|
||||||
|
type={type}
|
||||||
|
pinned={true}
|
||||||
|
id={id}
|
||||||
|
mid={mid}
|
||||||
|
setInviteChannelId={setInviteChannelId}
|
||||||
|
setDeleteChannelId={setDeleteId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul> : null}
|
||||||
<ul ref={ref} className="flex flex-1 flex-col gap-0.5 p-2 overflow-auto">
|
<ul ref={ref} className="flex flex-1 flex-col gap-0.5 p-2 overflow-auto">
|
||||||
<ViewportList
|
<ViewportList
|
||||||
initialPrerender={10}
|
initialPrerender={10}
|
||||||
|
|||||||
@@ -72,10 +72,7 @@ function ChatPage() {
|
|||||||
)}>
|
)}>
|
||||||
<Server readonly={isGuest} />
|
<Server readonly={isGuest} />
|
||||||
{isGuest ? <GuestSessionList /> : <SessionList tempSession={tmpSession} />}
|
{isGuest ? <GuestSessionList /> : <SessionList tempSession={tmpSession} />}
|
||||||
|
<RTCWidget id={+contextId} context={context} />
|
||||||
{isGuest ? <footer className="hidden md:block py-1 text-xs text-gray-300 dark:text-gray-700 text-center">
|
|
||||||
Host your own <a href="https://voce.chat" target="_blank" rel="noopener noreferrer" className="text-gray-400 dark:text-gray-600">voce.chat</a>
|
|
||||||
</footer> : <RTCWidget id={+contextId} context={context} />}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx(`right-container md:rounded-r-2xl w-full bg-white dark:!bg-gray-700`, placeholderVisible && "h-full flex-center", isMainPath && "hidden md:flex")}>
|
<div className={clsx(`right-container md:rounded-r-2xl w-full bg-white dark:!bg-gray-700`, placeholderVisible && "h-full flex-center", isMainPath && "hidden md:flex")}>
|
||||||
{voiceFullscreenVisible && <VoiceFullscreen id={contextId} context={context} />}
|
{voiceFullscreenVisible && <VoiceFullscreen id={contextId} context={context} />}
|
||||||
|
|||||||
@@ -32,11 +32,10 @@ import LazyIt from './lazy';
|
|||||||
import store, { useAppSelector } from "../app/store";
|
import store, { useAppSelector } from "../app/store";
|
||||||
import useDeviceToken from "../common/component/Notification/useDeviceToken";
|
import useDeviceToken from "../common/component/Notification/useDeviceToken";
|
||||||
import { vapidKey } from "../app/config";
|
import { vapidKey } from "../app/config";
|
||||||
import useTabBroadcast from "../common/hook/useTabBroadcast";
|
import RequireSingleTab from "../common/component/RequireSingleTab";
|
||||||
import InactiveScreen from "../common/component/InactiveScreen";
|
import InvitePrivate from "./invitePrivate";
|
||||||
let toastId: string;
|
let toastId: string;
|
||||||
const PageRoutes = () => {
|
const PageRoutes = () => {
|
||||||
|
|
||||||
const {
|
const {
|
||||||
ui: { online },
|
ui: { online },
|
||||||
fileMessages
|
fileMessages
|
||||||
@@ -59,6 +58,7 @@ const PageRoutes = () => {
|
|||||||
<HashRouter>
|
<HashRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/guest_login" element={<LazyIt><GuestLogin /></LazyIt>} />
|
<Route path="/guest_login" element={<LazyIt><GuestLogin /></LazyIt>} />
|
||||||
|
<Route path="/invite_private/:channel_id" element={<LazyIt><RequireAuth><InvitePrivate /></RequireAuth></LazyIt>} />
|
||||||
<Route path="/cb/:type/:payload" element={<LazyIt><CallbackPage /></LazyIt>} />
|
<Route path="/cb/:type/:payload" element={<LazyIt><CallbackPage /></LazyIt>} />
|
||||||
<Route path="/oauth/:token" element={<LazyIt><OAuthPage /></LazyIt>} />
|
<Route path="/oauth/:token" element={<LazyIt><OAuthPage /></LazyIt>} />
|
||||||
<Route
|
<Route
|
||||||
@@ -112,7 +112,10 @@ const PageRoutes = () => {
|
|||||||
element={
|
element={
|
||||||
<LazyIt>
|
<LazyIt>
|
||||||
<RequireAuth>
|
<RequireAuth>
|
||||||
<HomePage />
|
{/* 只允许活跃一个tab标签 */}
|
||||||
|
<RequireSingleTab>
|
||||||
|
<HomePage />
|
||||||
|
</RequireSingleTab>
|
||||||
</RequireAuth>
|
</RequireAuth>
|
||||||
</LazyIt>
|
</LazyIt>
|
||||||
}
|
}
|
||||||
@@ -199,11 +202,11 @@ const PageRoutes = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function ReduxRoutes() {
|
export default function ReduxRoutes() {
|
||||||
const { tabActive } = useTabBroadcast();
|
|
||||||
return (
|
return (
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Meta />
|
<Meta />
|
||||||
{tabActive ? <PageRoutes /> : <InactiveScreen />}
|
<PageRoutes />
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
||||||
|
import { useJoinPrivateChannelMutation, useLazyGetChannelQuery } from '../../app/services/channel';
|
||||||
|
import StyledButton from '../../common/component/styled/Button';
|
||||||
|
import { useCheckMagicTokenValidMutation } from '../../app/services/auth';
|
||||||
|
import { useAppSelector } from '../../app/store';
|
||||||
|
|
||||||
|
const InvitePrivate = () => {
|
||||||
|
const { channel_id } = useParams();
|
||||||
|
const server = useAppSelector(store => store.server);
|
||||||
|
const navigateTo = useNavigate();
|
||||||
|
const [joinChannel, { isLoading, data, isSuccess }] = useJoinPrivateChannelMutation();
|
||||||
|
const [fetchChannelInfo, { data: channel, isSuccess: fetchChannelSuccess }] = useLazyGetChannelQuery();
|
||||||
|
const [checkTokenInvalid, { data: isTokenValid, isLoading: checkingToken }] =
|
||||||
|
useCheckMagicTokenValidMutation();
|
||||||
|
let [searchParams] = useSearchParams(new URLSearchParams(location.search));
|
||||||
|
const magic_token = searchParams.get("magic_token") ?? "";
|
||||||
|
useEffect(() => {
|
||||||
|
if (channel_id) {
|
||||||
|
fetchChannelInfo(+channel_id);
|
||||||
|
}
|
||||||
|
}, [channel_id]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (magic_token) {
|
||||||
|
checkTokenInvalid(magic_token);
|
||||||
|
}
|
||||||
|
}, [magic_token]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (data && isSuccess) {
|
||||||
|
// joinChannel(data)
|
||||||
|
navigateTo(`/chat/channel/${data.gid}`);
|
||||||
|
}
|
||||||
|
}, [isSuccess, data]);
|
||||||
|
const handleJoin = async () => {
|
||||||
|
const resp = await joinChannel({ magic_token });
|
||||||
|
if ("error" in resp) {
|
||||||
|
if (resp.error.originalStatus === 409) {
|
||||||
|
// alert("The invite link is invalid or expired");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (!fetchChannelSuccess) return null;
|
||||||
|
return (
|
||||||
|
<div className="flex-center flex-col gap-4 h-screen overflow-x-hidden overflow-y-auto dark:bg-gray-700 dark:text-slate-100">
|
||||||
|
<div className="flex flex-col gap-4 items-center py-8 px-10 rounded-lg shadow-md bg-slate-100/30 dark:bg-gray-800 text-center">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<img src={server.logo} className='w-20 h-20' alt="server logo" />
|
||||||
|
<h2 className="text-2xl font-bold">
|
||||||
|
{server.name}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
{checkingToken ? "Checking..." : isTokenValid ? <>You are invited to join private channel <strong className='text-primary-400'>#{channel?.name}</strong></> : "The invite link is invalid or expired"}
|
||||||
|
</span>
|
||||||
|
<StyledButton disabled={isLoading || checkingToken || !isTokenValid} onClick={handleJoin}>Join</StyledButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InvitePrivate;
|
||||||
@@ -88,14 +88,6 @@ const navs = [
|
|||||||
name: "version",
|
name: "version",
|
||||||
component: <Version />
|
component: <Version />
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "faq",
|
|
||||||
link: "https://doc.voce.chat/faq",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "feedback",
|
|
||||||
link: `https://privoce.voce.chat/widget?from=${encodeURIComponent(location.host)}`,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,30 +1,36 @@
|
|||||||
|
import { FC, ChangeEvent } from "react";
|
||||||
import Tippy from "@tippyjs/react";
|
import Tippy from "@tippyjs/react";
|
||||||
import IconSearch from "../../assets/icons/search.svg";
|
import IconSearch from "../../assets/icons/search.svg";
|
||||||
import IconAdd from "../../assets/icons/add.svg";
|
import IconAdd from "../../assets/icons/add.svg";
|
||||||
import AddEntriesMenu from "../../common/component/AddEntriesMenu";
|
|
||||||
import Tooltip from "../../common/component/Tooltip";
|
|
||||||
import { FC, ChangeEvent } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useAppSelector } from "../../app/store";
|
||||||
|
import { compareVersion } from "../../common/utils";
|
||||||
|
import Tooltip from "../../common/component/Tooltip";
|
||||||
|
import AddEntriesMenu from "../../common/component/AddEntriesMenu";
|
||||||
type Props = {
|
type Props = {
|
||||||
input: string,
|
input: string,
|
||||||
|
openModal: () => void,
|
||||||
updateInput: (input: string) => void
|
updateInput: (input: string) => void
|
||||||
};
|
};
|
||||||
const Search: FC<Props> = ({ input, updateInput }) => {
|
const Search: FC<Props> = ({ input, updateInput, openModal }) => {
|
||||||
|
const serverVersion = useAppSelector(store => store.server.version);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
|
const handleInput = (evt: ChangeEvent<HTMLInputElement>) => {
|
||||||
updateInput(evt.target.value);
|
updateInput(evt.target.value);
|
||||||
};
|
};
|
||||||
|
const enableContact = compareVersion(serverVersion, "0.3.7") >= 0;
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<div className="hidden md:flex relative min-h-[56px] px-2 py-3 items-center justify-between gap-2 shadow-[rgb(0_0_0_/_10%)_0px_1px_0px] dark:border-b-gray-500">
|
<div className="hidden md:flex relative min-h-[56px] px-2 py-3 items-center justify-between gap-2 shadow-[rgb(0_0_0_/_10%)_0px_1px_0px] dark:border-b-gray-500">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<IconSearch className="dark:fill-gray-400 w-6 h-6 shrink-0" />
|
<IconSearch className="dark:fill-gray-400 w-6 h-6 shrink-0" />
|
||||||
<input value={input} placeholder={`${t("action.search")}...`} className="w-full text-sm bg-transparent dark:text-gray-50" onChange={handleInput} />
|
<input value={input} placeholder={`${t("action.search")}...`} className="w-full text-sm bg-transparent dark:text-gray-50 outline-none" onChange={handleInput} />
|
||||||
</div>
|
</div>
|
||||||
<Tooltip tip={t("more")} placement="bottom">
|
{enableContact ? <IconAdd onClick={openModal} role="button" className="dark:fill-gray-400" /> : <Tooltip tip={t("more")} placement="bottom">
|
||||||
<Tippy interactive placement="bottom-end" trigger="click" content={<AddEntriesMenu />}>
|
<Tippy interactive placement="bottom-end" trigger="click" content={<AddEntriesMenu />}>
|
||||||
<IconAdd role="button" className="dark:fill-gray-400" />
|
<IconAdd role="button" className="dark:fill-gray-400" />
|
||||||
</Tippy>
|
</Tippy>
|
||||||
</Tooltip>
|
</Tooltip>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { memo, useEffect, useRef } from "react";
|
import { memo, useEffect, useRef, useState } from "react";
|
||||||
import { NavLink, useParams, useLocation } from "react-router-dom";
|
import { NavLink, useParams, useLocation } from "react-router-dom";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
import { ViewportList } from 'react-viewport-list';
|
import { ViewportList } from 'react-viewport-list';
|
||||||
@@ -12,6 +12,7 @@ import BlankPlaceholder from "../../common/component/BlankPlaceholder";
|
|||||||
import useFilteredUsers from "../../common/hook/useFilteredUsers";
|
import useFilteredUsers from "../../common/hook/useFilteredUsers";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import GoBackNav from "../../common/component/GoBackNav";
|
import GoBackNav from "../../common/component/GoBackNav";
|
||||||
|
import SearchUser from "../../common/component/SearchUser";
|
||||||
|
|
||||||
function UsersPage() {
|
function UsersPage() {
|
||||||
const ref = useRef<HTMLDivElement | null>(
|
const ref = useRef<HTMLDivElement | null>(
|
||||||
@@ -19,6 +20,7 @@ function UsersPage() {
|
|||||||
);
|
);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
const { input, updateInput, users } = useFilteredUsers();
|
const { input, updateInput, users } = useFilteredUsers();
|
||||||
const { user_id } = useParams();
|
const { user_id } = useParams();
|
||||||
// 记住路由
|
// 记住路由
|
||||||
@@ -28,7 +30,9 @@ function UsersPage() {
|
|||||||
dispatch(updateRememberedNavs({ key: "user", path: pathname }));
|
dispatch(updateRememberedNavs({ key: "user", path: pathname }));
|
||||||
};
|
};
|
||||||
}, [pathname]);
|
}, [pathname]);
|
||||||
|
const toggleModal = () => {
|
||||||
|
setModalVisible(prev => !prev);
|
||||||
|
};
|
||||||
if (!users) return null;
|
if (!users) return null;
|
||||||
const isUserDetail = !!user_id;
|
const isUserDetail = !!user_id;
|
||||||
return (
|
return (
|
||||||
@@ -36,7 +40,8 @@ function UsersPage() {
|
|||||||
<div className={clsx("md:rounded-l-2xl bg-white dark:bg-gray-800 relative flex flex-col w-full md:w-auto md:min-w-[268px] shadow-[inset_-1px_0px_0px_rgba(0,_0,_0,_0.1)]",
|
<div className={clsx("md:rounded-l-2xl bg-white dark:bg-gray-800 relative flex flex-col w-full md:w-auto md:min-w-[268px] shadow-[inset_-1px_0px_0px_rgba(0,_0,_0,_0.1)]",
|
||||||
isUserDetail && "hidden md:flex"
|
isUserDetail && "hidden md:flex"
|
||||||
)}>
|
)}>
|
||||||
<Search input={input} updateInput={updateInput} />
|
<Search input={input} updateInput={updateInput} openModal={toggleModal} />
|
||||||
|
{modalVisible && <SearchUser closeModal={toggleModal} />}
|
||||||
<div className="flex flex-col md:gap-1 px-2 pt-3 pb-20 md:py-3 overflow-scroll" ref={ref}>
|
<div className="flex flex-col md:gap-1 px-2 pt-3 pb-20 md:py-3 overflow-scroll" ref={ref}>
|
||||||
<ViewportList
|
<ViewportList
|
||||||
viewportRef={ref}
|
viewportRef={ref}
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
import { PriceType } from "./common";
|
import { PriceType } from "./common";
|
||||||
import { User } from "./user";
|
import { User } from "./user";
|
||||||
|
|
||||||
export interface Server {
|
export interface Server extends SystemCommon {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-4
@@ -1,4 +1,4 @@
|
|||||||
import { User } from "./user";
|
import { ContactInfo, User } from "./user";
|
||||||
import { Channel } from "./channel";
|
import { Channel } from "./channel";
|
||||||
import { ContentType } from "./message";
|
import { ContentType } from "./message";
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ export type AutoDeleteMsgForUser = {
|
|||||||
export type AutoDeleteMsgForGroup = {
|
export type AutoDeleteMsgForGroup = {
|
||||||
gid: number; expires_in: number | null
|
gid: number; expires_in: number | null
|
||||||
}
|
}
|
||||||
export interface UserSettingsEvent {
|
export type UserSettingsEvent = {
|
||||||
type: "user_settings";
|
type: "user_settings";
|
||||||
mute_users?: MuteUser[];
|
mute_users?: MuteUser[];
|
||||||
mute_groups?: MuteChannel[];
|
mute_groups?: MuteChannel[];
|
||||||
@@ -68,13 +68,17 @@ export type AutoDeleteSettingForChannels = {
|
|||||||
burn_after_reading_groups: AutoDeleteMsgForGroup[]
|
burn_after_reading_groups: AutoDeleteMsgForGroup[]
|
||||||
}
|
}
|
||||||
export type AutoDeleteMessageSettingDTO = AutoDeleteSettingForUsers | AutoDeleteSettingForChannels
|
export type AutoDeleteMessageSettingDTO = AutoDeleteSettingForUsers | AutoDeleteSettingForChannels
|
||||||
export interface UserSettingsChangedEvent {
|
export type UserSettingsChangedEvent = {
|
||||||
type: "user_settings_changed";
|
type: "user_settings_changed";
|
||||||
from_device?: string;
|
from_device?: string;
|
||||||
add_mute_users?: MuteUser[];
|
add_mute_users?: MuteUser[];
|
||||||
remove_mute_users?: number[];
|
remove_mute_users?: number[];
|
||||||
add_mute_groups?: MuteChannel[];
|
add_mute_groups?: MuteChannel[];
|
||||||
|
add_contacts?: { target_uid: number, info: ContactInfo }[];
|
||||||
|
remove_contacts?: number[];
|
||||||
remove_mute_groups?: number[];
|
remove_mute_groups?: number[];
|
||||||
|
add_pin_chats?: PinChat[];
|
||||||
|
remove_pin_chats?: PinChatTarget[];
|
||||||
read_index_users?: { uid: number; mid: number }[];
|
read_index_users?: { uid: number; mid: number }[];
|
||||||
read_index_groups?: { gid: number; mid: number }[];
|
read_index_groups?: { gid: number; mid: number }[];
|
||||||
burn_after_reading_users?: AutoDeleteMsgForUser[];
|
burn_after_reading_users?: AutoDeleteMsgForUser[];
|
||||||
@@ -85,6 +89,18 @@ export interface RelatedGroupsEvent {
|
|||||||
type: "related_groups";
|
type: "related_groups";
|
||||||
groups: Channel[];
|
groups: Channel[];
|
||||||
}
|
}
|
||||||
|
export type PinChatTargetUser = {
|
||||||
|
uid: number;
|
||||||
|
};
|
||||||
|
export type PinChatTargetChannel = {
|
||||||
|
gid: number;
|
||||||
|
};
|
||||||
|
export type PinChatTarget = PinChatTargetUser | PinChatTargetChannel;
|
||||||
|
export type PinChat = { target: PinChatTarget[], updated_at: number }
|
||||||
|
export interface PinnedChatsEvent {
|
||||||
|
type: "pinned_chats";
|
||||||
|
chats: PinChat[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface NormalMessage {
|
export interface NormalMessage {
|
||||||
mid: number;
|
mid: number;
|
||||||
@@ -215,4 +231,4 @@ export type ServerEvent =
|
|||||||
| PinnedMessageUpdatedEvent
|
| PinnedMessageUpdatedEvent
|
||||||
| HeartbeatEvent
|
| HeartbeatEvent
|
||||||
| GroupClearEvent
|
| GroupClearEvent
|
||||||
;
|
| PinnedChatsEvent;
|
||||||
|
|||||||
@@ -19,6 +19,21 @@ export interface User {
|
|||||||
is_bot?: boolean;
|
is_bot?: boolean;
|
||||||
log_id?: number;
|
log_id?: number;
|
||||||
}
|
}
|
||||||
|
export type ContactAction = "add" | "remove" | "block" | "unblock";
|
||||||
|
export type ContactStatus = "added" | "blocked" | "";
|
||||||
|
export type ContactInfo = {
|
||||||
|
status: ContactStatus;
|
||||||
|
created_at: number;
|
||||||
|
updated_at: number;
|
||||||
|
}
|
||||||
|
export interface ContactResponse {
|
||||||
|
target_uid: number;
|
||||||
|
target_info: User;
|
||||||
|
contact_info: ContactInfo
|
||||||
|
}
|
||||||
|
export interface Contact extends User {
|
||||||
|
status: ContactStatus
|
||||||
|
}
|
||||||
export type UserStatus = "normal" | "frozen";
|
export type UserStatus = "normal" | "frozen";
|
||||||
export type UserDevice = {
|
export type UserDevice = {
|
||||||
device: string;
|
device: string;
|
||||||
|
|||||||
+10
-1
@@ -23,9 +23,18 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
speaking: "speaking 0.5s infinite cubic-bezier(.36, .11, .89, .32) alternate"
|
speaking: "speaking 0.5s infinite cubic-bezier(.36, .11, .89, .32) alternate",
|
||||||
|
zoomIn: "zoomIn 0.2s ease-in forwards"
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
|
zoomIn: {
|
||||||
|
from: {
|
||||||
|
transform: `scale(0.2)`
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
transform: `scale(1)`
|
||||||
|
}
|
||||||
|
},
|
||||||
fadeInUp: {
|
fadeInUp: {
|
||||||
from: {
|
from: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user