diff --git a/package.json b/package.json index daa66328..74613fd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vocechat-web", - "version": "0.7.29", + "version": "0.7.30", "homepage": "https://voce.chat", "dependencies": { "@metamask/onboarding": "^1.0.1", diff --git a/public/locales/en/setting.json b/public/locales/en/setting.json index 406d0d3e..73242be8 100644 --- a/public/locales/en/setting.json +++ b/public/locales/en/setting.json @@ -53,6 +53,12 @@ "everyone": "Everyone", "invite": "Invitation Link Only" }, + "admin_create_group": { + "title": "Only Admins create group", + "desc": "If enabled, only admins can create group.", + "enable": "Enable", + "disable": "Disabled" + }, "guest_mode": { "title": "Guest Mode", "desc": "If enabled, visitors will see public channels on this server.", diff --git a/src/app/slices/server.ts b/src/app/slices/server.ts index 6f48e0d6..1a4b5d93 100644 --- a/src/app/slices/server.ts +++ b/src/app/slices/server.ts @@ -24,8 +24,10 @@ const initialState: StoredServer = { show_user_online_status: false, webclient_auto_update: true, contact_verification_enable: false, + only_admin_can_create_group: false, chat_layout_mode: "Left", - loginConfig: null + loginConfig: null, + ext_setting: null }; const serverSlice = createSlice({ @@ -50,7 +52,9 @@ const serverSlice = createSlice({ webclient_auto_update = true, contact_verification_enable = false, chat_layout_mode = "Left", - loginConfig = state.loginConfig || null + only_admin_can_create_group = false, + loginConfig = state.loginConfig || null, + ext_setting = null } = action.payload || {}; return { version: state.version || version, @@ -62,8 +66,10 @@ const serverSlice = createSlice({ show_user_online_status, webclient_auto_update, contact_verification_enable, + only_admin_can_create_group, chat_layout_mode, - loginConfig + loginConfig, + ext_setting }; }, updateInfo(state, action: PayloadAction>) { diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx index 0f1f04be..17760d52 100644 --- a/src/components/Profile/index.tsx +++ b/src/components/Profile/index.tsx @@ -25,6 +25,7 @@ const Profile: FC = ({ uid, type = "embed", cid }) => { const { t } = useTranslation("member"); const { t: ct } = useTranslation(); const { + canDM, canCopyEmail, copyEmail, startCall, @@ -36,7 +37,6 @@ const Profile: FC = ({ uid, type = "embed", cid }) => { canUpdateRole, updateRole } = useUserOperation({ uid, cid }); - const data = useAppSelector((store) => store.users.byId[uid], shallowEqual); if (!data) return null; // console.log("profile", data); @@ -54,6 +54,7 @@ const Profile: FC = ({ uid, type = "embed", cid }) => { `flex-center flex-col gap-1 z-[99] mt-20 select-none`, isCard ? "p-4 w-[280px] bg-white dark:bg-gray-800 drop-shadow rounded-md" : "md:w-[432px]" ); + return (
= ({ uid, type = "embed", cid }) => { name={name} />

- {name} #{uid} + {name} {canDM && #{uid}}

- {email} + {canCopyEmail && ( + {email} + )} {/*

{introduction}

*/} -
    - -
  • - - {t("send_msg")} -
  • -
    - {agoraEnabled && type == "embed" && ( -
  • - - {t("call")} -
  • - )} - - } + {canDM && ( +
      -
    • - - {ct("more")} -
    • - -
    + +
  • + + {t("send_msg")} +
  • +
    + {agoraEnabled && type == "embed" && ( +
  • + + {t("call")} +
  • + )} + + } + > +
  • + + {ct("more")} +
  • +
    +
+ )}
); }; diff --git a/src/components/SettingBlock.tsx b/src/components/SettingBlock.tsx index ea14540e..b31bb373 100644 --- a/src/components/SettingBlock.tsx +++ b/src/components/SettingBlock.tsx @@ -8,7 +8,7 @@ type Props = { const SettingBlock = ({ title, desc, children }: Props) => { return ( -
+

{title}

{desc}

{children} diff --git a/src/hooks/useUserOperation.ts b/src/hooks/useUserOperation.ts index 687890f1..2b945c82 100644 --- a/src/hooks/useUserOperation.ts +++ b/src/hooks/useUserOperation.ts @@ -48,7 +48,7 @@ const useUserOperation = ({ uid, cid }: IProps) => { shallowEqual ); const loginUser = useAppSelector((store) => store.authData.user, shallowEqual); - + const { show_email = true, dm_to_member = true } = channel ?? {}; useEffect(() => { setPassedUid(uid ?? loginUser?.uid); }, [uid, loginUser]); @@ -103,7 +103,7 @@ const useUserOperation = ({ uid, cid }: IProps) => { aside: "voice" as const }; dispatch(updateDMVisibleAside(data)); - // 实时显示calling box + // 实时显示 calling box if (!joinedAtThisContext) { dispatch(updateCallInfo({ from: loginUser?.uid ?? 0, to: uid, calling: false })); } @@ -160,8 +160,9 @@ const useUserOperation = ({ uid, cid }: IProps) => { startChat, removeFromChannel: handleRemoveFromChannel, canRemoveFromChannel, - canCopyEmail: !!user?.email, + canCopyEmail: !!user?.email && show_email, copyEmail, + canDM: dm_to_member, startCall }; }; diff --git a/src/routes/setting/Overview/index.tsx b/src/routes/setting/Overview/index.tsx index 6d7be12f..e82662a2 100644 --- a/src/routes/setting/Overview/index.tsx +++ b/src/routes/setting/Overview/index.tsx @@ -15,6 +15,7 @@ import OnlineStatus from "./OnlineStatus"; import MessageSound from "./MessageSound"; import Server from "./Server"; import { shallowEqual } from "react-redux"; +import OnlyAdminCreateGroup from "./OnlyAdminCreateGroup"; export default function Overview() { const { t } = useTranslation("setting"); @@ -53,6 +54,9 @@ export default function Overview() { }} /> + {/* 只有 admin 能创建群组 */} + + {/* 访客模式 */} + {/* 是否显示在线提示 */} {/* 会话布局 */} diff --git a/src/routes/settingChannel/Overview.tsx b/src/routes/settingChannel/Overview.tsx deleted file mode 100644 index 4440dccb..00000000 --- a/src/routes/settingChannel/Overview.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import { ChangeEvent, useEffect, useState } from "react"; -import toast from "react-hot-toast"; -import { useTranslation } from "react-i18next"; - -import { - useChangeChannelTypeMutation, - useGetChannelQuery, - useUpdateChannelMutation, - useUpdateIconMutation -} from "@/app/services/channel"; -import { useAppSelector } from "@/app/store"; -import { Channel } from "@/types/channel"; -import AvatarUploader from "@/components/AvatarUploader"; -import SaveTip from "@/components/SaveTip"; -import Input from "@/components/styled/Input"; -import Label from "@/components/styled/Label"; -import Radio from "@/components/styled/Radio"; -import Textarea from "@/components/styled/Textarea"; -import IconChannel from "@/assets/icons/channel.svg"; -import { shallowEqual } from "react-redux"; - -export default function Overview({ id = 0 }) { - const { t } = useTranslation("setting", { keyPrefix: "channel" }); - const { t: ct } = useTranslation(); - const loginUser = useAppSelector((store) => store.authData.user, shallowEqual); - const channel = useAppSelector((store) => store.channels.byId[id], shallowEqual); - const { data, refetch } = useGetChannelQuery(id); - const [changed, setChanged] = useState(false); - const [values, setValues] = useState(); - const [updateChannelIcon] = useUpdateIconMutation(); - const [updateChannel, { isSuccess: updated }] = useUpdateChannelMutation(); - const [changeChannelType, { isSuccess: changeTypeSuccess }] = useChangeChannelTypeMutation(); - const handleUpdate = () => { - if (!values) return; - const { name, description } = values; - updateChannel({ id, name, description }); - }; - - const handleChange = (evt: ChangeEvent) => { - const newValue = evt.target.value; - const { type = "" } = evt.target.dataset; - setValues((prev) => { - if (!prev) return prev; - return { ...prev, [type]: newValue }; - }); - }; - - const updateIcon = (image: File) => { - updateChannelIcon({ gid: id, image }); - }; - - const handleReset = () => { - setValues(data); - }; - - useEffect(() => { - if (data) { - setValues(data); - } - }, [data]); - - useEffect(() => { - if (data && values) { - const { name, description } = values; - const { name: oName, description: oDescription } = data; - if (oName !== name || oDescription !== description) { - setChanged(true); - } else { - setChanged(false); - } - } - }, [data, values]); - - useEffect(() => { - if (updated) { - toast.success(ct("tip.update")); - refetch(); - } - }, [updated]); - useEffect(() => { - if (changeTypeSuccess) { - toast.success(ct("tip.update")); - } - }, [changeTypeSuccess]); - - if (!values || !id || !channel) return null; - const { name, description } = values; - const readOnly = !loginUser?.is_admin && channel?.owner != loginUser?.uid; - const inputClass = `w-full flex flex-col items-start gap-2 relative`; - return ( -
- -
-
- - #{id} -
-
- - - -
-
- -