feat: switches
This commit is contained in:
@@ -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<Partial<StoredServer>>) {
|
||||
|
||||
@@ -25,6 +25,7 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
|
||||
const { t } = useTranslation("member");
|
||||
const { t: ct } = useTranslation();
|
||||
const {
|
||||
canDM,
|
||||
canCopyEmail,
|
||||
copyEmail,
|
||||
startCall,
|
||||
@@ -36,7 +37,6 @@ const Profile: FC<Props> = ({ 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<Props> = ({ 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 (
|
||||
<div className={containerClass}>
|
||||
<Avatar
|
||||
@@ -64,80 +65,86 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
|
||||
name={name}
|
||||
/>
|
||||
<h2 className="text-lg select-text font-bold text-gray-900 dark:text-white">
|
||||
{name} <span className="font-normal text-gray-500">#{uid}</span>
|
||||
{name} {canDM && <span className="font-normal text-gray-500">#{uid}</span>}
|
||||
</h2>
|
||||
<span className="text-sm text-gray-400 dark:text-gray-200 select-text">{email}</span>
|
||||
{canCopyEmail && (
|
||||
<span className="text-sm text-gray-400 dark:text-gray-200 select-text">{email}</span>
|
||||
)}
|
||||
{/* <p className="intro">{introduction}</p> */}
|
||||
<ul className={clsx("mt-6 flex flex-col md:flex-row items-center gap-2", isCard && "pb-0.5")}>
|
||||
<NavLink to={`/chat/dm/${uid}`}>
|
||||
<li className={`${iconClass} icon chat`}>
|
||||
<IconMessage />
|
||||
<span>{t("send_msg")}</span>
|
||||
</li>
|
||||
</NavLink>
|
||||
{agoraEnabled && type == "embed" && (
|
||||
<li role="button" onClick={startCall} className={`${iconClass} icon chat`}>
|
||||
<IconCall className="fill-primary-400" />
|
||||
<span>{t("call")}</span>
|
||||
</li>
|
||||
)}
|
||||
<Tippy
|
||||
disabled={!hasMore}
|
||||
interactive
|
||||
popperOptions={{ strategy: "fixed" }}
|
||||
placement="right"
|
||||
trigger="click"
|
||||
hideOnClick={true}
|
||||
content={
|
||||
<ContextMenu
|
||||
items={
|
||||
[
|
||||
agoraEnabled &&
|
||||
type == "card" && {
|
||||
title: t("call"),
|
||||
handler: startCall
|
||||
},
|
||||
canCopyEmail && {
|
||||
title: t("copy_email"),
|
||||
handler: copyEmail
|
||||
},
|
||||
canUpdateRole && {
|
||||
title: t("roles"),
|
||||
handler: updateRole,
|
||||
subs: [
|
||||
{
|
||||
title: t("set_normal"),
|
||||
checked: !isAdmin,
|
||||
handler: updateRole
|
||||
},
|
||||
{
|
||||
title: t("set_admin"),
|
||||
checked: isAdmin,
|
||||
handler: updateRole
|
||||
}
|
||||
]
|
||||
},
|
||||
canRemoveFromChannel && {
|
||||
title: t("remove_from_channel"),
|
||||
danger: true,
|
||||
handler: removeFromChannel
|
||||
},
|
||||
canRemoveFromServer && {
|
||||
title: t("remove"),
|
||||
handler: removeUser,
|
||||
danger: true
|
||||
}
|
||||
].filter(Boolean) as Item[]
|
||||
}
|
||||
/>
|
||||
}
|
||||
{canDM && (
|
||||
<ul
|
||||
className={clsx("mt-6 flex flex-col md:flex-row items-center gap-2", isCard && "pb-0.5")}
|
||||
>
|
||||
<li className={`${iconClass} icon ${hasMore ? "" : "text-gray-500"}`}>
|
||||
<IconMore className={hasMore ? "fill-primary-500" : ""} />
|
||||
<span>{ct("more")}</span>
|
||||
</li>
|
||||
</Tippy>
|
||||
</ul>
|
||||
<NavLink to={`/chat/dm/${uid}`}>
|
||||
<li className={`${iconClass} icon chat`}>
|
||||
<IconMessage />
|
||||
<span>{t("send_msg")}</span>
|
||||
</li>
|
||||
</NavLink>
|
||||
{agoraEnabled && type == "embed" && (
|
||||
<li role="button" onClick={startCall} className={`${iconClass} icon chat`}>
|
||||
<IconCall className="fill-primary-400" />
|
||||
<span>{t("call")}</span>
|
||||
</li>
|
||||
)}
|
||||
<Tippy
|
||||
disabled={!hasMore}
|
||||
interactive
|
||||
popperOptions={{ strategy: "fixed" }}
|
||||
placement="right"
|
||||
trigger="click"
|
||||
hideOnClick={true}
|
||||
content={
|
||||
<ContextMenu
|
||||
items={
|
||||
[
|
||||
agoraEnabled &&
|
||||
type == "card" && {
|
||||
title: t("call"),
|
||||
handler: startCall
|
||||
},
|
||||
canCopyEmail && {
|
||||
title: t("copy_email"),
|
||||
handler: copyEmail
|
||||
},
|
||||
canUpdateRole && {
|
||||
title: t("roles"),
|
||||
handler: updateRole,
|
||||
subs: [
|
||||
{
|
||||
title: t("set_normal"),
|
||||
checked: !isAdmin,
|
||||
handler: updateRole
|
||||
},
|
||||
{
|
||||
title: t("set_admin"),
|
||||
checked: isAdmin,
|
||||
handler: updateRole
|
||||
}
|
||||
]
|
||||
},
|
||||
canRemoveFromChannel && {
|
||||
title: t("remove_from_channel"),
|
||||
danger: true,
|
||||
handler: removeFromChannel
|
||||
},
|
||||
canRemoveFromServer && {
|
||||
title: t("remove"),
|
||||
handler: removeUser,
|
||||
danger: true
|
||||
}
|
||||
].filter(Boolean) as Item[]
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<li className={`${iconClass} icon ${hasMore ? "" : "text-gray-500"}`}>
|
||||
<IconMore className={hasMore ? "fill-primary-500" : ""} />
|
||||
<span>{ct("more")}</span>
|
||||
</li>
|
||||
</Tippy>
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ type Props = {
|
||||
|
||||
const SettingBlock = ({ title, desc, children }: Props) => {
|
||||
return (
|
||||
<div className="text-sm">
|
||||
<div className="text-sm w-full">
|
||||
<p className="text-gray-600 dark:text-gray-100 font-semibold">{title}</p>
|
||||
<p className="flex justify-between w-full text-gray-400 mb-2 text-xs">{desc}</p>
|
||||
{children}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
}}
|
||||
/>
|
||||
</SettingBlock>
|
||||
{/* 只有 admin 能创建群组 */}
|
||||
|
||||
<OnlyAdminCreateGroup />
|
||||
{/* 访客模式 */}
|
||||
<SettingBlock title={t("overview.guest_mode.title")} desc={t("overview.guest_mode.desc")}>
|
||||
<StyledRadio
|
||||
@@ -64,6 +68,7 @@ export default function Overview() {
|
||||
}}
|
||||
/>
|
||||
</SettingBlock>
|
||||
|
||||
{/* 是否显示在线提示 */}
|
||||
<OnlineStatus />
|
||||
{/* 会话布局 */}
|
||||
|
||||
@@ -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<Channel>();
|
||||
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<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
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 (
|
||||
<div className="relative w-[512px] flex flex-col gap-6 h-full">
|
||||
<AvatarUploader type="channel" url={channel?.icon} name={name} uploadImage={updateIcon} />
|
||||
<div className="flex flex-col gap-6 items-start">
|
||||
<div className={"flex items-center gap-1"}>
|
||||
<Label htmlFor="name">{t("id")}</Label>
|
||||
<span className="text-gray-500">#{id}</span>
|
||||
</div>
|
||||
<div className={inputClass}>
|
||||
<Label htmlFor="name">{t("name")}</Label>
|
||||
<Input
|
||||
disabled={readOnly}
|
||||
className="!pl-8"
|
||||
data-type="name"
|
||||
onChange={handleChange}
|
||||
value={name}
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder={t("name")}
|
||||
/>
|
||||
<IconChannel className="absolute bottom-2.5 left-2 dark:fill-gray-300" />
|
||||
</div>
|
||||
<div className={inputClass}>
|
||||
<Label htmlFor="desc">{t("topic")}</Label>
|
||||
<Textarea
|
||||
disabled={readOnly}
|
||||
data-type="description"
|
||||
onChange={handleChange}
|
||||
value={description ?? ""}
|
||||
rows={4}
|
||||
name="name"
|
||||
id="name"
|
||||
placeholder={t("topic_placeholder")}
|
||||
/>
|
||||
</div>
|
||||
{!readOnly && loginUser.is_admin && (
|
||||
<div className={inputClass}>
|
||||
<Label htmlFor="desc">{t("visibility")}</Label>
|
||||
<Radio
|
||||
options={[t("public"), t("private")]}
|
||||
values={["true", "false"]}
|
||||
value={String(channel.is_public)}
|
||||
onChange={(v: string) => {
|
||||
// console.log("wtff", typeof v, v);
|
||||
changeChannelType({ is_public: v.toLowerCase() === "true", id });
|
||||
// handleGuestToggle(v);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{changed && <SaveTip saveHandler={handleUpdate} resetHandler={handleReset} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+20
-1
@@ -28,6 +28,12 @@ export interface Channel {
|
||||
is_public: boolean;
|
||||
avatar_updated_at: number;
|
||||
pinned_messages: PinnedMessage[];
|
||||
// ext switches
|
||||
show_email: boolean;
|
||||
dm_to_member: boolean;
|
||||
add_friend: boolean;
|
||||
only_owner_can_send_msg: boolean;
|
||||
ext_setting: null | string;
|
||||
}
|
||||
|
||||
export interface CreateChannelDTO {
|
||||
@@ -37,7 +43,20 @@ export interface CreateChannelDTO {
|
||||
is_public: boolean;
|
||||
}
|
||||
|
||||
export interface ChannelDTO extends Partial<Pick<Channel, "owner" | "description" | "name">> {
|
||||
export interface ChannelDTO
|
||||
extends Partial<
|
||||
Pick<
|
||||
Channel,
|
||||
| "owner"
|
||||
| "description"
|
||||
| "name"
|
||||
| "show_email"
|
||||
| "add_friend"
|
||||
| "only_owner_can_send_msg"
|
||||
| "dm_to_member"
|
||||
| "ext_setting"
|
||||
>
|
||||
> {
|
||||
id: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface SystemCommon {
|
||||
contact_verification_enable?: boolean;
|
||||
chat_layout_mode?: ChatLayout;
|
||||
max_file_expiry_mode?: MessageExpireMode;
|
||||
only_admin_can_create_group: boolean;
|
||||
ext_setting: null | string;
|
||||
}
|
||||
export interface GithubAuthConfig {
|
||||
client_id?: string;
|
||||
|
||||
Reference in New Issue
Block a user