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
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "vocechat-web", "name": "vocechat-web",
"version": "0.7.29", "version": "0.7.30",
"homepage": "https://voce.chat", "homepage": "https://voce.chat",
"dependencies": { "dependencies": {
"@metamask/onboarding": "^1.0.1", "@metamask/onboarding": "^1.0.1",
+6
View File
@@ -53,6 +53,12 @@
"everyone": "Everyone", "everyone": "Everyone",
"invite": "Invitation Link Only" "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": { "guest_mode": {
"title": "Guest Mode", "title": "Guest Mode",
"desc": "If enabled, visitors will see public channels on this server.", "desc": "If enabled, visitors will see public channels on this server.",
+9 -3
View File
@@ -24,8 +24,10 @@ const initialState: StoredServer = {
show_user_online_status: false, show_user_online_status: false,
webclient_auto_update: true, webclient_auto_update: true,
contact_verification_enable: false, contact_verification_enable: false,
only_admin_can_create_group: false,
chat_layout_mode: "Left", chat_layout_mode: "Left",
loginConfig: null loginConfig: null,
ext_setting: null
}; };
const serverSlice = createSlice({ const serverSlice = createSlice({
@@ -50,7 +52,9 @@ const serverSlice = createSlice({
webclient_auto_update = true, webclient_auto_update = true,
contact_verification_enable = false, contact_verification_enable = false,
chat_layout_mode = "Left", chat_layout_mode = "Left",
loginConfig = state.loginConfig || null only_admin_can_create_group = false,
loginConfig = state.loginConfig || null,
ext_setting = null
} = action.payload || {}; } = action.payload || {};
return { return {
version: state.version || version, version: state.version || version,
@@ -62,8 +66,10 @@ const serverSlice = createSlice({
show_user_online_status, show_user_online_status,
webclient_auto_update, webclient_auto_update,
contact_verification_enable, contact_verification_enable,
only_admin_can_create_group,
chat_layout_mode, chat_layout_mode,
loginConfig loginConfig,
ext_setting
}; };
}, },
updateInfo(state, action: PayloadAction<Partial<StoredServer>>) { updateInfo(state, action: PayloadAction<Partial<StoredServer>>) {
+10 -3
View File
@@ -25,6 +25,7 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
const { t } = useTranslation("member"); const { t } = useTranslation("member");
const { t: ct } = useTranslation(); const { t: ct } = useTranslation();
const { const {
canDM,
canCopyEmail, canCopyEmail,
copyEmail, copyEmail,
startCall, startCall,
@@ -36,7 +37,6 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
canUpdateRole, canUpdateRole,
updateRole updateRole
} = useUserOperation({ uid, cid }); } = useUserOperation({ uid, cid });
const data = useAppSelector((store) => store.users.byId[uid], shallowEqual); const data = useAppSelector((store) => store.users.byId[uid], shallowEqual);
if (!data) return null; if (!data) return null;
// console.log("profile", data); // 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`, `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]" isCard ? "p-4 w-[280px] bg-white dark:bg-gray-800 drop-shadow rounded-md" : "md:w-[432px]"
); );
return ( return (
<div className={containerClass}> <div className={containerClass}>
<Avatar <Avatar
@@ -64,11 +65,16 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
name={name} name={name}
/> />
<h2 className="text-lg select-text font-bold text-gray-900 dark:text-white"> <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> </h2>
{canCopyEmail && (
<span className="text-sm text-gray-400 dark:text-gray-200 select-text">{email}</span> <span className="text-sm text-gray-400 dark:text-gray-200 select-text">{email}</span>
)}
{/* <p className="intro">{introduction}</p> */} {/* <p className="intro">{introduction}</p> */}
<ul className={clsx("mt-6 flex flex-col md:flex-row items-center gap-2", isCard && "pb-0.5")}> {canDM && (
<ul
className={clsx("mt-6 flex flex-col md:flex-row items-center gap-2", isCard && "pb-0.5")}
>
<NavLink to={`/chat/dm/${uid}`}> <NavLink to={`/chat/dm/${uid}`}>
<li className={`${iconClass} icon chat`}> <li className={`${iconClass} icon chat`}>
<IconMessage /> <IconMessage />
@@ -138,6 +144,7 @@ const Profile: FC<Props> = ({ uid, type = "embed", cid }) => {
</li> </li>
</Tippy> </Tippy>
</ul> </ul>
)}
</div> </div>
); );
}; };
+1 -1
View File
@@ -8,7 +8,7 @@ type Props = {
const SettingBlock = ({ title, desc, children }: Props) => { const SettingBlock = ({ title, desc, children }: Props) => {
return ( 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="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> <p className="flex justify-between w-full text-gray-400 mb-2 text-xs">{desc}</p>
{children} {children}
+3 -2
View File
@@ -48,7 +48,7 @@ const useUserOperation = ({ uid, cid }: IProps) => {
shallowEqual shallowEqual
); );
const loginUser = useAppSelector((store) => store.authData.user, shallowEqual); const loginUser = useAppSelector((store) => store.authData.user, shallowEqual);
const { show_email = true, dm_to_member = true } = channel ?? {};
useEffect(() => { useEffect(() => {
setPassedUid(uid ?? loginUser?.uid); setPassedUid(uid ?? loginUser?.uid);
}, [uid, loginUser]); }, [uid, loginUser]);
@@ -160,8 +160,9 @@ const useUserOperation = ({ uid, cid }: IProps) => {
startChat, startChat,
removeFromChannel: handleRemoveFromChannel, removeFromChannel: handleRemoveFromChannel,
canRemoveFromChannel, canRemoveFromChannel,
canCopyEmail: !!user?.email, canCopyEmail: !!user?.email && show_email,
copyEmail, copyEmail,
canDM: dm_to_member,
startCall startCall
}; };
}; };
+5
View File
@@ -15,6 +15,7 @@ import OnlineStatus from "./OnlineStatus";
import MessageSound from "./MessageSound"; import MessageSound from "./MessageSound";
import Server from "./Server"; import Server from "./Server";
import { shallowEqual } from "react-redux"; import { shallowEqual } from "react-redux";
import OnlyAdminCreateGroup from "./OnlyAdminCreateGroup";
export default function Overview() { export default function Overview() {
const { t } = useTranslation("setting"); const { t } = useTranslation("setting");
@@ -53,6 +54,9 @@ export default function Overview() {
}} }}
/> />
</SettingBlock> </SettingBlock>
{/* 只有 admin 能创建群组 */}
<OnlyAdminCreateGroup />
{/* 访客模式 */} {/* 访客模式 */}
<SettingBlock title={t("overview.guest_mode.title")} desc={t("overview.guest_mode.desc")}> <SettingBlock title={t("overview.guest_mode.title")} desc={t("overview.guest_mode.desc")}>
<StyledRadio <StyledRadio
@@ -64,6 +68,7 @@ export default function Overview() {
}} }}
/> />
</SettingBlock> </SettingBlock>
{/* 是否显示在线提示 */} {/* 是否显示在线提示 */}
<OnlineStatus /> <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; is_public: boolean;
avatar_updated_at: number; avatar_updated_at: number;
pinned_messages: PinnedMessage[]; 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 { export interface CreateChannelDTO {
@@ -37,7 +43,20 @@ export interface CreateChannelDTO {
is_public: boolean; 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; id: number;
} }
+2
View File
@@ -14,6 +14,8 @@ export interface SystemCommon {
contact_verification_enable?: boolean; contact_verification_enable?: boolean;
chat_layout_mode?: ChatLayout; chat_layout_mode?: ChatLayout;
max_file_expiry_mode?: MessageExpireMode; max_file_expiry_mode?: MessageExpireMode;
only_admin_can_create_group: boolean;
ext_setting: null | string;
} }
export interface GithubAuthConfig { export interface GithubAuthConfig {
client_id?: string; client_id?: string;