feat: warn admin when creating public channel with guest mode enabled

Show a warning overlay when admin creates a public channel and guest
mode is on, offering three actions: disable guest mode, switch to
private channel, or dismiss permanently (stored in localStorage).

chore: bump version to 0.9.93
This commit is contained in:
haorwen
2026-06-26 23:51:29 +08:00
parent c0e7ab34cb
commit 6ebce14a18
4 changed files with 103 additions and 8 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "vocechat-web",
"version": "0.9.92",
"version": "0.9.93",
"homepage": "https://voce.chat",
"dependencies": {
"@metamask/onboarding": "^1.0.1",
+9 -1
View File
@@ -76,5 +76,13 @@
"remark": "Add friend nickname",
"remark_clear": "Reset friend nickname",
"remark_intro": "Find a friend faster with a personal nickname. It will only be visible to you in your direct messages.",
"remark_placeholder": "Set Nickname"
"remark_placeholder": "Set Nickname",
"guest_mode_warning": {
"title": "Guest Mode is Enabled",
"desc": "Unauthenticated visitors can see messages in public channels. You may disable guest mode or switch to a private channel instead.",
"disable_guest": "Disable Guest Mode",
"switch_private": "Switch to Private Channel",
"dont_remind": "Don't remind me again"
}
}
+9 -1
View File
@@ -75,5 +75,13 @@
"remark": "备注",
"remark_clear": "重置备注",
"remark_intro": "给联系人备注,方便日后查找,备注仅您可见",
"remark_placeholder": "备注该用户"
"remark_placeholder": "备注该用户",
"guest_mode_warning": {
"title": "访客模式已开启",
"desc": "未登录的访客可以看到公共频道的消息流。您可以前往关闭访客模式,或改为创建私有频道。",
"disable_guest": "关闭访客模式",
"switch_private": "改为私有频道",
"dont_remind": "不再提醒"
}
}
+84 -5
View File
@@ -6,8 +6,10 @@ import i18n from "@/i18n";
import clsx from "clsx";
import { useCreateChannelMutation, useSendChannelMsgMutation } from "@/app/services/channel";
import { useGetLoginConfigQuery, useUpdateLoginConfigMutation } from "@/app/services/server";
import { useAppSelector } from "@/app/store";
import { CreateChannelDTO } from "@/types/channel";
import { LoginConfig } from "@/types/server";
import useFilteredUsers from "@/hooks/useFilteredUsers";
import ChannelIcon from "../ChannelIcon";
import Modal from "../Modal";
@@ -17,6 +19,44 @@ import StyledToggle from "../styled/Toggle";
import User from "../User";
import { shallowEqual } from "react-redux";
const GUEST_WARNING_DISMISSED_KEY = "vocechat_public_channel_guest_warning_dismissed";
interface GuestModeWarningProps {
onDisableGuest: () => void;
onSwitchPrivate: () => void;
onDismiss: () => void;
}
const GuestModeWarning: FC<GuestModeWarningProps> = ({ onDisableGuest, onSwitchPrivate, onDismiss }) => {
const { t } = useTranslation("chat");
return (
<div className="absolute inset-0 z-10 flex items-center justify-center bg-black/40 rounded-lg">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-sm w-full mx-4 shadow-xl">
<h4 className="font-semibold text-gray-700 dark:text-white text-base mb-2">
{t("guest_mode_warning.title")}
</h4>
<p className="text-sm text-gray-500 dark:text-gray-300 mb-5">
{t("guest_mode_warning.desc")}
</p>
<div className="flex flex-col gap-2">
<Button onClick={onDisableGuest} className="text-sm w-full justify-center">
{t("guest_mode_warning.disable_guest")}
</Button>
<Button onClick={onSwitchPrivate} className="text-sm w-full justify-center cancel">
{t("guest_mode_warning.switch_private")}
</Button>
<button
onClick={onDismiss}
className="text-xs text-gray-400 hover:text-gray-500 dark:hover:text-gray-300 mt-1 text-center"
>
{t("guest_mode_warning.dont_remind")}
</button>
</div>
</div>
</div>
);
};
interface Props {
personal?: boolean;
closeModal: () => void;
@@ -34,6 +74,10 @@ const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
members: loginUser?.uid ? [loginUser.uid] : [],
is_public: !personal
});
const [showGuestWarning, setShowGuestWarning] = useState(false);
const { data: loginConfig } = useGetLoginConfigQuery();
const [updateLoginConfig] = useUpdateLoginConfigMutation();
const { users, input, updateInput } = useFilteredUsers();
const [createChannel, { isSuccess, isError, isLoading, data: newChannel }] =
@@ -45,17 +89,42 @@ const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
return { ...prev, is_public: !is_public };
});
};
const doCreate = (channelData: CreateChannelDTO) => {
const payload = { ...channelData };
if (payload.is_public) {
delete payload.members;
}
createChannel(payload);
};
const handleCreate = () => {
// todo: add field validation (maxLength, text format, trim)
if (!data.name) {
toast("please input channel name");
return;
}
if (data.is_public) {
// 公共频道 不必有 members
delete data.members;
const guestEnabled = (loginConfig as LoginConfig | undefined)?.guest ?? false;
const warningDismissed = localStorage.getItem(GUEST_WARNING_DISMISSED_KEY) === "1";
if (data.is_public && loginUser?.is_admin && guestEnabled && !warningDismissed) {
setShowGuestWarning(true);
return;
}
createChannel(data);
doCreate(data);
};
const handleWarningDisableGuest = () => {
if (loginConfig) {
updateLoginConfig({ ...(loginConfig as LoginConfig), guest: false });
}
setShowGuestWarning(false);
doCreate(data);
};
const handleWarningDismiss = () => {
localStorage.setItem(GUEST_WARNING_DISMISSED_KEY, "1");
setShowGuestWarning(false);
doCreate(data);
};
// todo: delete the following code and use common error handler instead
@@ -101,7 +170,17 @@ const ChannelModal: FC<Props> = ({ personal = false, closeModal }) => {
const loginUid = loginUser.uid;
return (
<Modal>
<div className="flex flex-col md:flex-row max-h-screen md:max-h-[402px] bg-white dark:bg-gray-800 drop-shadow rounded-lg">
<div className="relative flex flex-col md:flex-row max-h-screen md:max-h-[402px] bg-white dark:bg-gray-800 drop-shadow rounded-lg">
{showGuestWarning && (
<GuestModeWarning
onDisableGuest={handleWarningDisableGuest}
onSwitchPrivate={() => {
setShowGuestWarning(false);
setData((prev) => ({ ...prev, is_public: false }));
}}
onDismiss={handleWarningDismiss}
/>
)}
{!is_public && (
<div className="md:w-[260px] md:shadow-[inset_-1px_0px_0px_rgba(0,_0,_0,_0.1)]">
<div className="sticky top-0 z-[99] rounded-tl-lg shadow-[0px_1px_0px_rgba(0,_0,_0,_0.1)] p-2 w-[calc(100%_-_1px)]">