fix(channel): enforce read-only for members and polish chat info
Some checks failed
CI / test (push) Failing after 26s
Some checks failed
CI / test (push) Failing after 26s
- block send/forward in channels for member role on backend - expose my_role in chat payload for client-side permissions - hide message composer for channel members and show read-only notice - hide members management sections for private/saved chats in info panel - return enriched chat detail via serialize_chat_for_user
This commit is contained in:
@@ -36,6 +36,7 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
|
||||
const myRole = useMemo(() => members.find((m) => m.user_id === me?.id)?.role, [members, me?.id]);
|
||||
const isGroupLike = chat?.type === "group" || chat?.type === "channel";
|
||||
const showMembersSection = Boolean(chat && isGroupLike && !chat.is_saved);
|
||||
const canManageMembers = Boolean(isGroupLike && (myRole === "owner" || myRole === "admin"));
|
||||
const canChangeRoles = Boolean(isGroupLike && myRole === "owner");
|
||||
|
||||
@@ -139,56 +140,74 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
{chat.description ? <p className="mt-1 text-xs text-slate-400">{chat.description}</p> : null}
|
||||
</div>
|
||||
|
||||
<div className="mb-3 rounded-lg border border-slate-700/70 bg-slate-800/60 p-3">
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-300">Members ({members.length})</p>
|
||||
<div className="tg-scrollbar max-h-56 space-y-2 overflow-auto">
|
||||
{members.map((member) => {
|
||||
const user = memberUsers[member.user_id];
|
||||
return (
|
||||
<div className="rounded border border-slate-700/60 bg-slate-900/60 p-2" key={member.id}>
|
||||
<p className="truncate text-sm font-semibold">{user?.name || `user #${member.user_id}`}</p>
|
||||
<p className="truncate text-xs text-slate-400">@{user?.username || "unknown"}</p>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<select
|
||||
className="flex-1 rounded bg-slate-800 px-2 py-1 text-xs"
|
||||
disabled={!canChangeRoles || member.user_id === me?.id}
|
||||
value={member.role}
|
||||
onChange={async (e) => {
|
||||
try {
|
||||
await updateChatMemberRole(chatId, member.user_id, e.target.value as ChatMember["role"]);
|
||||
await refreshMembers(chatId);
|
||||
} catch {
|
||||
setError("Failed to update role");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="member">member</option>
|
||||
<option value="admin">admin</option>
|
||||
<option value="owner">owner</option>
|
||||
</select>
|
||||
{canManageMembers && member.user_id !== me?.id ? (
|
||||
<button
|
||||
className="rounded bg-red-500 px-2 py-1 text-xs font-semibold text-white"
|
||||
onClick={async () => {
|
||||
{showMembersSection ? (
|
||||
<div className="mb-3 rounded-lg border border-slate-700/70 bg-slate-800/60 p-3">
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-300">Members ({members.length})</p>
|
||||
<div className="tg-scrollbar max-h-56 space-y-2 overflow-auto">
|
||||
{members.map((member) => {
|
||||
const user = memberUsers[member.user_id];
|
||||
return (
|
||||
<div className="rounded border border-slate-700/60 bg-slate-900/60 p-2" key={member.id}>
|
||||
<p className="truncate text-sm font-semibold">{user?.name || `user #${member.user_id}`}</p>
|
||||
<p className="truncate text-xs text-slate-400">@{user?.username || "unknown"}</p>
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<select
|
||||
className="flex-1 rounded bg-slate-800 px-2 py-1 text-xs"
|
||||
disabled={!canChangeRoles || member.user_id === me?.id}
|
||||
value={member.role}
|
||||
onChange={async (e) => {
|
||||
try {
|
||||
await removeChatMember(chatId, member.user_id);
|
||||
await updateChatMemberRole(chatId, member.user_id, e.target.value as ChatMember["role"]);
|
||||
await refreshMembers(chatId);
|
||||
} catch {
|
||||
setError("Failed to remove member");
|
||||
setError("Failed to update role");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
) : null}
|
||||
<option value="member">member</option>
|
||||
<option value="admin">admin</option>
|
||||
<option value="owner">owner</option>
|
||||
</select>
|
||||
{canManageMembers && member.user_id !== me?.id ? (
|
||||
<button
|
||||
className="rounded bg-red-500 px-2 py-1 text-xs font-semibold text-white"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await removeChatMember(chatId, member.user_id);
|
||||
await refreshMembers(chatId);
|
||||
} catch {
|
||||
setError("Failed to remove member");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-3 rounded-lg border border-slate-700/70 bg-slate-800/60 p-3">
|
||||
{chat.is_saved ? (
|
||||
<p className="text-sm text-slate-300">Saved Messages is your personal cloud chat.</p>
|
||||
) : chat.type === "private" ? (
|
||||
<p className="text-sm text-slate-300">
|
||||
{chat.counterpart_is_online
|
||||
? "User is online"
|
||||
: chat.counterpart_last_seen_at
|
||||
? `Last seen ${formatLastSeen(chat.counterpart_last_seen_at)}`
|
||||
: "User is offline"}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-slate-300">No extra information.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canManageMembers ? (
|
||||
{showMembersSection && canManageMembers ? (
|
||||
<div className="mb-3 rounded-lg border border-slate-700/70 bg-slate-800/60 p-3">
|
||||
<p className="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-300">Add member</p>
|
||||
<input
|
||||
@@ -234,7 +253,7 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{chat.type === "group" || chat.type === "channel" ? (
|
||||
{showMembersSection && (chat.type === "group" || chat.type === "channel") ? (
|
||||
<button
|
||||
className="w-full rounded bg-slate-700 px-3 py-2 text-sm"
|
||||
onClick={async () => {
|
||||
@@ -258,3 +277,16 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
|
||||
document.body
|
||||
);
|
||||
}
|
||||
|
||||
function formatLastSeen(value: string): string {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return "recently";
|
||||
}
|
||||
return date.toLocaleString(undefined, {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit"
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user