feat: orphaned data cleanup, file deletion on message/account removal, and group DM improvements

- Add fileCleanup utility to delete uploaded files from disk on message/account deletion
- Add migration to clean orphaned DM channels, attachments, reactions, read states, and stale moderator refs
- Add FK constraints on dm_message_id in attachments and dm_reactions schema
- Make bans.bannedBy and voiceRestrictions.moderatorId nullable for deleted moderators
- Transfer group DM ownership on member leave or account deletion
- Fully clean up orphaned DM channels (zero members) including files
- Add "Leave Group" context menu for group DMs in ChannelSidebar
- Add leaveDm action to spaceStore
This commit is contained in:
Jannis Braun
2026-03-11 16:54:34 +01:00
parent 8c8767ba2c
commit 1889d45a07
8 changed files with 328 additions and 14 deletions
@@ -15,6 +15,7 @@ import { AudioManager } from '../../audio/AudioManager';
import { hasPermissionBit, PermissionBits } from '../../utils/permissions';
import { parseFederatedUsername, isSelf } from '../../utils/identity';
import { joinVoiceChannel, broadcastVoiceStatus, broadcastDeafenViaLiveKit } from '../../utils/voice';
import { ContextMenu } from '../ui/ContextMenu';
export function ChannelSidebar() {
const spaces = useSpaceStore((s) => s.spaces);
@@ -208,7 +209,7 @@ export function ChannelSidebar() {
? otherMembers.map(m => m.displayName ?? parseFederatedUsername(m.username).baseName).join(', ')
: otherMembers[0]?.displayName ?? otherMembers[0]?.username;
return (
const dmItem = (
<div
key={dm.id}
onClick={() => handleChannelClick(dm.id)}
@@ -281,6 +282,36 @@ export function ChannelSidebar() {
</button>
</div>
);
if (isGroup) {
return (
<ContextMenu
key={dm.id}
items={[
{
label: 'Leave Group',
danger: true,
icon: (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M10.09 15.59L11.5 17l5-5-5-5-1.41 1.41L12.67 11H3v2h9.67l-2.58 2.59zM19 3H5a2 2 0 00-2 2v4h2V5h14v14H5v-4H3v4a2 2 0 002 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z" />
</svg>
),
onClick: () => {
if (currentChannelId === dm.id) {
navigate('/channels/@me');
setCurrentChannel(null);
}
useSpaceStore.getState().leaveDm(dm.id);
},
},
]}
>
{dmItem}
</ContextMenu>
);
}
return dmItem;
})}
{dmChannels.length === 0 && (
<p className="px-2 py-4 text-[13px] text-txt-tertiary italic opacity-60">No DM conversations yet.</p>