From e7bee14a910f6c45a879c16b21d4d49767efac94 Mon Sep 17 00:00:00 2001 From: klopez4212 Date: Wed, 23 Sep 2026 10:25:42 +0100 Subject: [PATCH 1/8] feat(channels): let people remove DMs from the sidebar Signed-off-by: klopez4212 --- docs/assets/dm-hide/dm-row-default.png | Bin 0 -> 1305 bytes docs/assets/dm-hide/dm-row-hover.png | Bin 0 -> 1761 bytes src/bundled/channels/ChannelSidebarItem.tsx | 3 + .../channels/ChannelSidebarRow.module.css | 22 ++- .../channels/ChannelSidebarRow.test.tsx | 27 +++ src/bundled/channels/ChannelSidebarRow.tsx | 15 ++ src/bundled/channels/ChannelsPage.tsx | 10 +- src/bundled/channels/sidebar-sections.test.ts | 5 + src/bundled/channels/sidebar-sections.ts | 5 +- src/bundled/channels/useHiddenDms.test.tsx | 165 ++++++++++++++++++ src/bundled/channels/useHiddenDms.ts | 127 ++++++++++++++ .../relay/unread-invalidation.test.ts | 8 +- src/features/relay/unread.test.ts | 4 + src/features/relay/unread.ts | 15 ++ tests/browser/sidebar-unread.spec.mjs | 55 ++++++ 15 files changed, 456 insertions(+), 5 deletions(-) create mode 100644 docs/assets/dm-hide/dm-row-default.png create mode 100644 docs/assets/dm-hide/dm-row-hover.png create mode 100644 src/bundled/channels/useHiddenDms.test.tsx create mode 100644 src/bundled/channels/useHiddenDms.ts diff --git a/docs/assets/dm-hide/dm-row-default.png b/docs/assets/dm-hide/dm-row-default.png new file mode 100644 index 0000000000000000000000000000000000000000..26c912747957910c0139f7dad635100353e4aca8 GIT binary patch literal 1305 zcmV+!1?KvRP)>%1?d^>r2zl`zLlClqWKGBr zk~JYiNY;c5Az2eLgk(*~5Rx?^LrB(y3?W$)GDH%xzPr1do1446y%oWAa&oe`xcKt& zB3aiTk$xT|h5E8yUS7`2%ZrYV&dki5nwo-TXJ;olIoZU-#Lv&Kt*z}J=P4*CFf}y| z4i3g=R#uks%Y0#7S65e|p`jKQ76AbPW@cu}5lX}*PKrbeTZ5=6>;C@!#Kc5vYwMz- zA`cIb!otFahK7fS2MD^myE`c8aqZ-Q8U~J3HLz z+}vE)pP!!*Dk8+QClQx8DH1Jg&7#g~jYk&WiqkaE*Vm7Yjh&d7;4BUf4p_NIM@M<) zxxc?ZJUsOA@!>4q-riVUE-o$vcWrKNV#v9)w6r`uJz1kfT;ilioUk>HIJXjEblu>&VE+_4W04#+8*7*tsDjzOb->eUN7!JPG)`xw+viY|U<@Km?beq2Xs? zi0{&1iUbe)i>gwwD z_VypY$XF;4#&{R4QY22;8t4Do+FC|MdU|>i5)uLf1JURyDJel|h(;zB z@297yu&^*xC-}n1LPbRdmJQ^WXDU}$*XHJC6dzdZ>+0&Jr>Fb+`mm@7?!piu!G(K_ z7A*EPJb`Ewpa77FYinx@n;tU8?)cx7BEiFk1#K2=1&)r6IM8E(!^;}BA%@o)cpti6oQIV065hxhP4D>pb?Ks!R#Khpsmz9;Vhivrw;^N};^z`ug z*=*?TqixjE(t=YBa)Ar(+#LSA0HoaZES1|A!4JzHS~~@lESD6o*(=o+ZT_Bh-hqVWK=}uzI9GcPH}Ou zo15E57g|X9`T4`c!<+@Z?XIpaycGEBwAa_ypOqlOy6}Bb`h4u`!$f=}Dk>^IKAs_jfL3clhS2|86EcKk zO~??EH6cSt)`ScpSramZWKGBrk~JYiegOag|Nm1U;m80000v1!K~w_(vzwfATpKu> P00000NkvXXu0mjfo2q#w literal 0 HcmV?d00001 diff --git a/docs/assets/dm-hide/dm-row-hover.png b/docs/assets/dm-hide/dm-row-hover.png new file mode 100644 index 0000000000000000000000000000000000000000..71291e4621d2bbb7ae79b66ea4f3b4749126fd39 GIT binary patch literal 1761 zcmV<71|Io|P)=hd-Dkvz5Xp#>}AP`6( z`I10FLOP_7Knm&QrM$e~?v3NJ+015lXJ#jG{~v~#b9U~Wz4O24o^xjAhIDmxDVpf$ z=xA$e`_BQ7KbDYii=dz&K_0Q?(5utMSvH%EZKwktF4r4Vk^k#}A|c@);F7mI_Sj%Z zNJv;%m>_yjxTm!swDipq76;@G4-fAtcSGx@rX~UpMMAsH@QV@SEghYP^u37BZ zX-)Xi+S;l}Nce|bpum)*q@>2iMiW#O z6%{EdDQ+V!i>Tbf;{6bCLaXTZ*7$A3C#0pNojZ4K*REYBPo9jAk4I8iSa|T@!6{Rw ztXsG4-Me>vZ0Ev-3sa{~-Mo1-+f%1bIZtHzs_N?MwrtrlbLPwq8#YXvHqF^Wi>S=$ zA|uQ;BK)YYulGu>g6PBM+mn~?-@hL?aA5rS@kfpvnLBsx#fujoK782H(z0a9lGm?a zZ{NQC=FOWwfBy8?&VvUJ=%D-e?-vvlJbn7~^XJdEZ{IdTMZX_9bSOJJJ1Q!Q?cTk6 zFI~DMMx#Yk=5X8Vce=H@$h?i3XjxyX^+y?Ym}#<_UvAj%@%)<9}&YnLuv%DbmGK`$&)7w38Z@S<_$5z z=+UFK&$anbt4>TzguW`L!QjDzH*VY*7Z;~WnD`(%gIl8=#$0J>>B5BzRSA7pRaI3} zQ=_{oH#e6#X8;gIc%7cb79J?oLv z7-RFnAiVXEAwyh*5fKr{oh5Lc3`3(GVsWmH^{RxZMsW;E-Bmzw^pvxT-wUl3ROWP% zL1r72GU7VVoH_IA)vM^}Xo6cfJ$CF^k7r?wP4OONgjrcxF2anA4CLw|6i-f0W~$O? zhmoK-0vJ^y_9GlHK_y<1v4|>wU8K9&I%3kKNn5vW{qf_+wr$&-0|af@d>Chp+D)4_ zso4qm^y$;Ufdf~qS|tuCD)9q?khN>qYP2(9!h}JC1~FZ#l9G}VCOBd#-Br`4Pv_7@ zC4?4HnbSoEnQh#I7{dBrzkco9xswV+adB~9PO%3ey}gL74N4y$KYsk@4~T&%eJGAt zkRe4;sk*wFSwp)KM%3)tvoTjxV~FzEpD|;`fB^$^S8)&6e*5-KT$;PWWHM*Y9K~_Z zte`TdiwrW`pdf?`J$v?yd>2Nsee&c9d8Eg)Fedow)vMlCLcV_ex_|%v6)RTk+qdu7 zv154UQKLrTym_oc^@Hq$e1cxMcI_Gwn8rN=q@LsfBp;!np;V(15)$6NeJe6weN{{* zn1qUZObPbNl`D)uN?_sP;TBP0Gm{=YCT4ttx=44kkx;ClD&k_c|0pjnfBg6{Pemw` zb2wte2zH)7e-6P&+cZwZQwKvnub#;K{{8#LjT>yK^(lWq0 zJ$v@R_#HcTELyba@ZrO=X3f%`WXFsd6B85j^5sih3)N7j8-r9>%PnC{Qp9CSxp(g# z!_PV@eBt%$*Dqhb42KNuB7M!~LFW1M=h@fxU*3XY!zJ+0YIP3`8#W9s@sC!nT*)x& z?OaB({WAj0Rr(CF(W6I?GRO5)Q9|M$5}c@#)YQ~3U%uciJT{9By$yGLRs3JZp4)UM zuqw*2nVFdoQRxMi?FWVpB-1^-uB@yaIdWv*$1R-!*bJO>FBvz=e)_)BXztz8b3kno5=F`JOuIoxnvrbH~O zAV1fY=r0iN*d>H^zxod8V{2=xT&zsO;-Fq078cf1uC=HX%$7?_O85h@YMDVxOKQZ- zp4Uo~3js^`50e)H{vQAU|NnZT0i^%{00v1!K~w_(`*!c>z)ovJ00000NkvXXu0mjf DSHEwR literal 0 HcmV?d00001 diff --git a/src/bundled/channels/ChannelSidebarItem.tsx b/src/bundled/channels/ChannelSidebarItem.tsx index a94735cf9..7f39d002a 100644 --- a/src/bundled/channels/ChannelSidebarItem.tsx +++ b/src/bundled/channels/ChannelSidebarItem.tsx @@ -29,6 +29,7 @@ export const ChannelSidebarItem = memo(function ChannelSidebarItem({ onSelect, onNewSession, onOpenThread, + onHideDm, }: { channel: ChannelSummary; session: RelaySession; @@ -43,6 +44,7 @@ export const ChannelSidebarItem = memo(function ChannelSidebarItem({ onSelect: (id: string) => void; onNewSession: (id: string) => void; onOpenThread: (channelId: string, rootId: string) => void; + onHideDm?: (id: string) => void; }) { const Icon = channel.channelType === "dm" @@ -97,6 +99,7 @@ export const ChannelSidebarItem = memo(function ChannelSidebarItem({ onPrepare={(id) => session.channels.prepare?.(id)} onSelect={onSelect} onNewSession={onNewSession} + {...(onHideDm ? { onHideDm } : {})} /> ); }); diff --git a/src/bundled/channels/ChannelSidebarRow.module.css b/src/bundled/channels/ChannelSidebarRow.module.css index df6b322ff..007660ffd 100644 --- a/src/bundled/channels/ChannelSidebarRow.module.css +++ b/src/bundled/channels/ChannelSidebarRow.module.css @@ -8,7 +8,7 @@ background: transparent; transition: color var(--duration-state) var(--easing-state), - background-color var(--duration-state) var(--easing-state); + background-color 90ms cubic-bezier(0.23, 1, 0.32, 1); } .row:hover:not([data-selected]) { background: var(--affordance-subtle); @@ -27,12 +27,32 @@ opacity: 0; pointer-events: none; flex-shrink: 0; + transition: opacity 90ms cubic-bezier(0.23, 1, 0.32, 1); +} +.row .remove { + position: absolute; + inset-block: 0; + right: 0; + z-index: 1; + display: flex; + align-items: center; + justify-content: flex-end; + width: calc(var(--size-row) + var(--space-control-inset)); + border-radius: inherit; + background-color: inherit; } .row:is(:hover, :has(:focus-visible)) .more, .row .more:has([data-popup-open]) { opacity: 1; pointer-events: auto; } +.row .remove [data-icon-variant] { + color: var(--text-subtle); + transition-duration: 90ms; +} +.row .remove [data-icon-variant]:is(:hover, :focus-visible) { + color: var(--text-standard); +} .iconSpace { width: 17px; flex: 0 0 17px; diff --git a/src/bundled/channels/ChannelSidebarRow.test.tsx b/src/bundled/channels/ChannelSidebarRow.test.tsx index 8db2d9f7c..e2df9a6b5 100644 --- a/src/bundled/channels/ChannelSidebarRow.test.tsx +++ b/src/bundled/channels/ChannelSidebarRow.test.tsx @@ -138,6 +138,33 @@ it("hides session actions when the Sessions plugin is unavailable", () => { screen.queryByRole("button", { name: "More options for Engineering" }), ).not.toBeInTheDocument(); }); +it("offers a separate hide action only for DM rows", async () => { + const onHideDm = vi.fn(); + const onSelect = vi.fn(); + render( + {}} + icon={} + sessions={[]} + sessionsEnabled={false} + draft={false} + draftSelected={false} + onSelect={onSelect} + onPrepare={() => {}} + onNewSession={() => {}} + onHideDm={onHideDm} + />, + ); + const remove = screen.getByRole("button", { + name: "Remove Alice from DMs", + }); + expect(remove).not.toHaveAttribute("title"); + await userEvent.setup().click(remove); + expect(onHideDm).toHaveBeenCalledWith("dm"); + expect(onSelect).not.toHaveBeenCalled(); +}); it("opens saved child sessions and retained drafts without a channel icon", async () => { const user = userEvent.setup(); const callbacks = mount(); diff --git a/src/bundled/channels/ChannelSidebarRow.tsx b/src/bundled/channels/ChannelSidebarRow.tsx index 2d372f705..c4bb4a469 100644 --- a/src/bundled/channels/ChannelSidebarRow.tsx +++ b/src/bundled/channels/ChannelSidebarRow.tsx @@ -4,6 +4,7 @@ import { CaretDownIcon, CaretRightIcon, DotsThreeVerticalIcon, + XIcon, } from "../../shared/design-system/icons/index"; import { useId, useRef, type ReactElement, type ReactNode } from "react"; import { Menu } from "@base-ui/react/menu"; @@ -27,6 +28,7 @@ export function ChannelSidebarRow({ onSelect, onPrepare, onNewSession, + onHideDm, }: { channel: ChannelSummary; icon: ReactNode; @@ -43,6 +45,7 @@ export function ChannelSidebarRow({ onSelect: (id: string) => void; onPrepare: (id: string) => void; onNewSession: (id: string) => void; + onHideDm?: (id: string) => void; }) { const starting = useRef(false); const childrenId = useId(); @@ -162,6 +165,18 @@ export function ChannelSidebarRow({ )} + {channel.channelType === "dm" && onHideDm && ( + + onHideDm(channel.id)} + icon={ + )}