feat: switches

This commit is contained in:
Tristan Yang
2024-09-11 18:16:44 +08:00
parent fc63ee1951
commit 10cc2663e2
10 changed files with 127 additions and 225 deletions
+9 -3
View File
@@ -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>>) {
+79 -72
View File
@@ -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>
);
};
+1 -1
View File
@@ -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}
+4 -3
View File
@@ -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
};
};
+5
View File
@@ -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 />
{/* 会话布局 */}
-144
View File
@@ -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
View File
@@ -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;
}
+2
View File
@@ -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;