diff --git a/CHANGELOG.md b/CHANGELOG.md
index 563f34e1dd9..bdcf0d86117 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+Changes in [3.95.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.95.0) (2024-03-14)
+=====================================================================================================
+## 🐛 Bug Fixes
+
+* Update `@vector-im/compound-design-tokens` in package.json ([#12340](https://github.com/matrix-org/matrix-react-sdk/pull/12340)).
+
 Changes in [3.94.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.94.0) (2024-03-12)
 =====================================================================================================
 ## ✨ Features
diff --git a/package.json b/package.json
index 36fc01ecb40..ee387bcf6e6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "matrix-react-sdk",
-    "version": "3.94.0",
+    "version": "3.95.0",
     "description": "SDK for matrix.org using React",
     "author": "matrix.org",
     "repository": {
@@ -75,7 +75,7 @@
         "@matrix-org/spec": "^1.7.0",
         "@sentry/browser": "^7.0.0",
         "@testing-library/react-hooks": "^8.0.1",
-        "@vector-im/compound-design-tokens": "^1.0.0",
+        "@vector-im/compound-design-tokens": "^1.2.0",
         "@vector-im/compound-web": "^3.1.1",
         "@zxcvbn-ts/core": "^3.0.4",
         "@zxcvbn-ts/language-common": "^3.0.4",
diff --git a/playwright/e2e/spaces/threads-activity-centre/index.ts b/playwright/e2e/spaces/threads-activity-centre/index.ts
index b0fcd4648a8..4360ddb9816 100644
--- a/playwright/e2e/spaces/threads-activity-centre/index.ts
+++ b/playwright/e2e/spaces/threads-activity-centre/index.ts
@@ -30,30 +30,30 @@ import { ElementAppPage } from "../../../pages/ElementAppPage";
  * - Invite the bot to both rooms and ensure that it has joined
  */
 export const test = base.extend<{
-    roomAlphaName?: string;
-    roomAlpha: { name: string; roomId: string };
-    roomBetaName?: string;
-    roomBeta: { name: string; roomId: string };
+    room1Name?: string;
+    room1: { name: string; roomId: string };
+    room2Name?: string;
+    room2: { name: string; roomId: string };
     msg: MessageBuilder;
     util: Helpers;
 }>({
     displayName: "Mae",
     botCreateOpts: { displayName: "Other User" },
 
-    roomAlphaName: "Room Alpha",
-    roomAlpha: async ({ roomAlphaName: name, app, user, bot }, use) => {
+    room1Name: "Room 1",
+    room1: async ({ room1Name: name, app, user, bot }, use) => {
         const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] });
         await use({ name, roomId });
     },
-    roomBetaName: "Room Beta",
-    roomBeta: async ({ roomBetaName: name, app, user, bot }, use) => {
+    room2Name: "Room 2",
+    room2: async ({ room2Name: name, app, user, bot }, use) => {
         const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] });
         await use({ name, roomId });
     },
     msg: async ({ page, app, util }, use) => {
         await use(new MessageBuilder(page, app, util));
     },
-    util: async ({ roomAlpha, roomBeta, page, app, bot }, use) => {
+    util: async ({ room1, room2, page, app, bot }, use) => {
         await use(new Helpers(page, app, bot));
     },
 });
@@ -265,6 +265,13 @@ export class Helpers {
         return this.getTacButton().click();
     }
 
+    /**
+     * Hover over the Threads Activity Centre button
+     */
+    hoverTacButton() {
+        return this.getTacButton().hover();
+    }
+
     /**
      * Click on a room in the Threads Activity Centre
      * @param name - room name
@@ -330,23 +337,27 @@ export class Helpers {
      * @param room1
      * @param room2
      * @param msg - MessageBuilder
+     * @param hasMention - whether to include a mention in the first message
      */
     async populateThreads(
         room1: { name: string; roomId: string },
         room2: { name: string; roomId: string },
         msg: MessageBuilder,
+        hasMention = true,
     ) {
-        await this.receiveMessages(room2, [
-            "Msg1",
-            msg.threadedOff("Msg1", {
-                "body": "User",
-                "format": "org.matrix.custom.html",
-                "formatted_body": "<a href='https://matrix.to/#/@user:localhost'>User</a>",
-                "m.mentions": {
-                    user_ids: ["@user:localhost"],
-                },
-            }),
-        ]);
+        if (hasMention) {
+            await this.receiveMessages(room2, [
+                "Msg1",
+                msg.threadedOff("Msg1", {
+                    "body": "User",
+                    "format": "org.matrix.custom.html",
+                    "formatted_body": "<a href='https://matrix.to/#/@user:localhost'>User</a>",
+                    "m.mentions": {
+                        user_ids: ["@user:localhost"],
+                    },
+                }),
+            ]);
+        }
         await this.receiveMessages(room2, ["Msg2", msg.threadedOff("Msg2", "Resp2")]);
         await this.receiveMessages(room1, ["Msg3", msg.threadedOff("Msg3", "Resp3")]);
     }
diff --git a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts
index eb4d7c8df06..93094073b31 100644
--- a/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts
+++ b/playwright/e2e/spaces/threads-activity-centre/threadsActivityCentre.spec.ts
@@ -35,7 +35,7 @@ test.describe("Threads Activity Centre", () => {
         await expect(util.getSpacePanel()).toMatchScreenshot("tac-button-expanded.png");
     });
 
-    test("should not show indicator when there is no thread", async ({ roomAlpha: room1, util }) => {
+    test("should not show indicator when there is no thread", async ({ room1, util }) => {
         // No indicator should be shown
         await util.assertNoTacIndicator();
 
@@ -46,11 +46,7 @@ test.describe("Threads Activity Centre", () => {
         await util.assertNoTacIndicator();
     });
 
-    test("should show a notification indicator when there is a message in a thread", async ({
-        roomAlpha: room1,
-        util,
-        msg,
-    }) => {
+    test("should show a notification indicator when there is a message in a thread", async ({ room1, util, msg }) => {
         await util.goTo(room1);
         await util.receiveMessages(room1, ["Msg1", msg.threadedOff("Msg1", "Resp1")]);
 
@@ -58,11 +54,7 @@ test.describe("Threads Activity Centre", () => {
         await util.assertNotificationTac();
     });
 
-    test("should show a highlight indicator when there is a mention in a thread", async ({
-        roomAlpha: room1,
-        util,
-        msg,
-    }) => {
+    test("should show a highlight indicator when there is a mention in a thread", async ({ room1, util, msg }) => {
         await util.goTo(room1);
         await util.receiveMessages(room1, [
             "Msg1",
@@ -80,7 +72,7 @@ test.describe("Threads Activity Centre", () => {
         await util.assertHighlightIndicator();
     });
 
-    test("should show the rooms with unread threads", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => {
+    test("should show the rooms with unread threads", async ({ room1, room2, util, msg }) => {
         await util.goTo(room2);
         await util.populateThreads(room1, room2, msg);
         // The indicator should be shown
@@ -97,7 +89,7 @@ test.describe("Threads Activity Centre", () => {
         await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-mix-unread.png");
     });
 
-    test("should update with a thread is read", async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => {
+    test("should update with a thread is read", async ({ room1, room2, util, msg }) => {
         await util.goTo(room2);
         await util.populateThreads(room1, room2, msg);
 
@@ -120,6 +112,17 @@ test.describe("Threads Activity Centre", () => {
         await expect(util.getTacPanel()).toMatchScreenshot("tac-panel-notification-unread.png");
     });
 
+    test("should order by recency after notification level", async ({ room1, room2, util, msg }) => {
+        await util.goTo(room2);
+        await util.populateThreads(room1, room2, msg, false);
+
+        await util.openTac();
+        await util.assertRoomsInTac([
+            { room: room1.name, notificationLevel: "notification" },
+            { room: room2.name, notificationLevel: "notification" },
+        ]);
+    });
+
     test("should block the Spotlight to open when the TAC is opened", async ({ util, page }) => {
         const toggleSpotlight = () => page.keyboard.press(`${CommandOrControl}+k`);
 
@@ -134,4 +137,14 @@ test.describe("Threads Activity Centre", () => {
         await toggleSpotlight();
         await expect(page.locator(".mx_SpotlightDialog")).not.toBeVisible();
     });
+
+    test("should have the correct hover state", async ({ util, page }) => {
+        await util.hoverTacButton();
+        await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered.png");
+
+        // Expand the space panel, hover the button and take a screenshot
+        await util.expandSpacePanel();
+        await util.hoverTacButton();
+        await expect(util.getSpacePanel()).toMatchScreenshot("tac-hovered-expanded.png");
+    });
 });
diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png
new file mode 100644
index 00000000000..37405cd821a
Binary files /dev/null and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-expanded-linux.png differ
diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png
new file mode 100644
index 00000000000..26f5bfdfa98
Binary files /dev/null and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-hovered-linux.png differ
diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png
index f02a943d6bd..ec5a8193d25 100644
Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-mix-unread-linux.png differ
diff --git a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png
index 1a94524d688..f0f6cee3e6a 100644
Binary files a/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png and b/playwright/snapshots/spaces/threads-activity-centre/threadsActivityCentre.spec.ts/tac-panel-notification-unread-linux.png differ
diff --git a/res/css/_common.pcss b/res/css/_common.pcss
index 568b9eea123..b750de0213c 100644
--- a/res/css/_common.pcss
+++ b/res/css/_common.pcss
@@ -335,6 +335,12 @@ legend {
     max-height: calc(100% - var(--cpd-space-12x));
     display: flex;
     flex-direction: column;
+
+    .mx_Dialog_lightbox & {
+        /* The lightbox isn't so much of a dialog as a fullscreen overlay. We
+        don't want the glass border. */
+        display: contents;
+    }
 }
 
 .mx_Dialog {
diff --git a/res/css/structures/_ThreadsActivityCentre.pcss b/res/css/structures/_ThreadsActivityCentre.pcss
index 4c61d32f776..76b38d6c076 100644
--- a/res/css/structures/_ThreadsActivityCentre.pcss
+++ b/res/css/structures/_ThreadsActivityCentre.pcss
@@ -25,6 +25,12 @@
     margin: 18px auto auto auto;
 
     &.expanded {
+        /**
+         * override compound default background color when hovered
+         * should disappear when the space panel will be migrated to compound
+         */
+        background-color: transparent !important;
+
         /* align with settings icon */
         margin-left: 21px;
 
diff --git a/src/components/views/avatars/DecoratedRoomAvatar.tsx b/src/components/views/avatars/DecoratedRoomAvatar.tsx
index ed8afe7b61a..5879dd3b1a7 100644
--- a/src/components/views/avatars/DecoratedRoomAvatar.tsx
+++ b/src/components/views/avatars/DecoratedRoomAvatar.tsx
@@ -35,7 +35,11 @@ interface IProps {
     room: Room;
     size: string;
     displayBadge?: boolean;
-    forceCount?: boolean;
+    /**
+     * If true, show nothing if the notification would only cause a dot to be shown rather than
+     * a badge. That is: only display badges and not dots. Default: false.
+     */
+    hideIfDot?: boolean;
     oobData?: IOOBData;
     viewAvatarOnClick?: boolean;
     tooltipProps?: {
@@ -178,14 +182,14 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
 
     public render(): React.ReactNode {
         // Spread the remaining props to make it work with compound component
-        const { room, size, displayBadge, forceCount, oobData, viewAvatarOnClick, tooltipProps, ...props } = this.props;
+        const { room, size, displayBadge, hideIfDot, oobData, viewAvatarOnClick, tooltipProps, ...props } = this.props;
 
         let badge: React.ReactNode;
         if (this.props.displayBadge && this.state.notificationState) {
             badge = (
                 <NotificationBadge
                     notification={this.state.notificationState}
-                    forceCount={this.props.forceCount}
+                    hideIfDot={this.props.hideIfDot}
                     roomId={this.props.room.roomId}
                 />
             );
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx
index cb414173387..b36fb972555 100644
--- a/src/components/views/rooms/EventTile.tsx
+++ b/src/components/views/rooms/EventTile.tsx
@@ -1311,7 +1311,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
                             <UnreadNotificationBadge
                                 room={room || undefined}
                                 threadId={this.props.mxEvent.getId()}
-                                type="dot"
+                                forceDot={true}
                             />
                         </div>
                         {isRenderingNotification && room ? (
diff --git a/src/components/views/rooms/ExtraTile.tsx b/src/components/views/rooms/ExtraTile.tsx
index 157bfc4d562..3bb3a21525a 100644
--- a/src/components/views/rooms/ExtraTile.tsx
+++ b/src/components/views/rooms/ExtraTile.tsx
@@ -52,7 +52,7 @@ export default function ExtraTile({
 
     let badge: JSX.Element | null = null;
     if (notificationState) {
-        badge = <NotificationBadge notification={notificationState} forceCount={false} />;
+        badge = <NotificationBadge notification={notificationState} />;
     }
 
     let name = displayName;
diff --git a/src/components/views/rooms/NotificationBadge.tsx b/src/components/views/rooms/NotificationBadge.tsx
index d152ab6a626..d4f7ee50407 100644
--- a/src/components/views/rooms/NotificationBadge.tsx
+++ b/src/components/views/rooms/NotificationBadge.tsx
@@ -28,10 +28,10 @@ interface IProps {
     notification: NotificationState;
 
     /**
-     * If true, the badge will show a count if at all possible. This is typically
-     * used to override the user's preference for things like room sublists.
+     * If true, show nothing if the notification would only cause a dot to be shown rather than
+     * a badge. That is: only display badges and not dots. Default: false.
      */
-    forceCount?: boolean;
+    hideIfDot?: boolean;
 
     /**
      * The room ID, if any, the badge represents.
@@ -48,7 +48,7 @@ interface IClickableProps extends IProps, React.InputHTMLAttributes<Element> {
 }
 
 interface IState {
-    showCounts: boolean; // whether to show counts. Independent of props.forceCount
+    showCounts: boolean; // whether to show counts.
 }
 
 export default class NotificationBadge extends React.PureComponent<XOR<IProps, IClickableProps>, IState> {
@@ -97,11 +97,12 @@ export default class NotificationBadge extends React.PureComponent<XOR<IProps, I
 
     public render(): ReactNode {
         /* eslint @typescript-eslint/no-unused-vars: ["error", { "ignoreRestSiblings": true }] */
-        const { notification, showUnsentTooltip, forceCount, onClick, tabIndex } = this.props;
+        const { notification, showUnsentTooltip, hideIfDot, onClick, tabIndex } = this.props;
 
         if (notification.isIdle && !notification.knocked) return null;
-        if (forceCount) {
-            if (!notification.hasUnreadCount) return null; // Can't render a badge
+        if (hideIfDot && notification.level < NotificationLevel.Notification) {
+            // This would just be a dot and we've been told not to show dots, so don't show it
+            if (!notification.hasUnreadCount) return null;
         }
 
         const commonProps: React.ComponentProps<typeof StatelessNotificationBadge> = {
diff --git a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx
index 69f756b3b7e..1d26083b6a0 100644
--- a/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx
+++ b/src/components/views/rooms/NotificationBadge/StatelessNotificationBadge.tsx
@@ -28,7 +28,12 @@ interface Props {
     count: number;
     level: NotificationLevel;
     knocked?: boolean;
-    type?: "badge" | "dot";
+    /**
+     * If true, where we would normally show a badge, we instead show a dot. No numeric count will
+     * be displayed (but may affect whether the the dot is displayed). See class doc
+     * for the difference between the two.
+     */
+    forceDot?: boolean;
 }
 
 interface ClickableProps extends Props {
@@ -39,8 +44,17 @@ interface ClickableProps extends Props {
     tabIndex?: number;
 }
 
+/**
+ * A notification indicator that conveys what activity / notifications the user has in whatever
+ * context it is being used.
+ *
+ * Can either be a 'badge': a small circle with a number in it (the 'count'), or a 'dot': a smaller, empty circle.
+ * The two can be used to convey the same meaning but in different contexts, for example: for unread
+ * notifications in the room list, it may have a green badge with the number of unread notifications,
+ * but somewhere else it may just have a green dot as a more compact representation of the same information.
+ */
 export const StatelessNotificationBadge = forwardRef<HTMLDivElement, XOR<Props, ClickableProps>>(
-    ({ symbol, count, level, knocked, type = "badge", ...props }, ref) => {
+    ({ symbol, count, level, knocked, forceDot = false, ...props }, ref) => {
         const hideBold = useSettingValue("feature_hidebold");
 
         // Don't show a badge if we don't need to
@@ -61,10 +75,12 @@ export const StatelessNotificationBadge = forwardRef<HTMLDivElement, XOR<Props,
             mx_NotificationBadge_visible: isEmptyBadge || knocked ? true : hasUnreadCount,
             mx_NotificationBadge_level_notification: level == NotificationLevel.Notification,
             mx_NotificationBadge_level_highlight: level >= NotificationLevel.Highlight,
-            mx_NotificationBadge_dot: (isEmptyBadge && !knocked) || type === "dot",
             mx_NotificationBadge_knocked: knocked,
-            mx_NotificationBadge_2char: type === "badge" && symbol && symbol.length > 0 && symbol.length < 3,
-            mx_NotificationBadge_3char: type === "badge" && symbol && symbol.length > 2,
+
+            // At most one of mx_NotificationBadge_dot, mx_NotificationBadge_2char, mx_NotificationBadge_3char
+            mx_NotificationBadge_dot: (isEmptyBadge && !knocked) || forceDot,
+            mx_NotificationBadge_2char: !forceDot && symbol && symbol.length > 0 && symbol.length < 3,
+            mx_NotificationBadge_3char: !forceDot && symbol && symbol.length > 2,
         });
 
         if (props.onClick) {
diff --git a/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx b/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx
index 5864a63be01..c3c8cf7df89 100644
--- a/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx
+++ b/src/components/views/rooms/NotificationBadge/UnreadNotificationBadge.tsx
@@ -23,11 +23,15 @@ import { StatelessNotificationBadge } from "./StatelessNotificationBadge";
 interface Props {
     room?: Room;
     threadId?: string;
-    type?: "badge" | "dot";
+    /**
+     * If true, where we would normally show a badge, we instead show a dot. No numeric count will
+     * be displayed.
+     */
+    forceDot?: boolean;
 }
 
-export function UnreadNotificationBadge({ room, threadId, type }: Props): JSX.Element {
+export function UnreadNotificationBadge({ room, threadId, forceDot }: Props): JSX.Element {
     const { symbol, count, level } = useUnreadNotifications(room, threadId);
 
-    return <StatelessNotificationBadge symbol={symbol} count={count} level={level} type={type} />;
+    return <StatelessNotificationBadge symbol={symbol} count={count} level={level} forceDot={forceDot} />;
 }
diff --git a/src/components/views/rooms/RoomBreadcrumbs.tsx b/src/components/views/rooms/RoomBreadcrumbs.tsx
index eca8da46240..cd31dbd8e79 100644
--- a/src/components/views/rooms/RoomBreadcrumbs.tsx
+++ b/src/components/views/rooms/RoomBreadcrumbs.tsx
@@ -61,7 +61,7 @@ const RoomBreadcrumbTile: React.FC<{ room: Room; onClick: (ev: ButtonEvent) => v
                 room={room}
                 size="32px"
                 displayBadge={true}
-                forceCount={true}
+                hideIfDot={true}
                 tooltipProps={{ tabIndex: isActive ? 0 : -1 }}
             />
         </AccessibleTooltipButton>
diff --git a/src/components/views/rooms/RoomSublist.tsx b/src/components/views/rooms/RoomSublist.tsx
index 4e84bee0be2..c8ad9d4acab 100644
--- a/src/components/views/rooms/RoomSublist.tsx
+++ b/src/components/views/rooms/RoomSublist.tsx
@@ -657,7 +657,7 @@ export default class RoomSublist extends React.Component<IProps, IState> {
 
                     const badge = (
                         <NotificationBadge
-                            forceCount={true}
+                            hideIfDot={true}
                             notification={this.notificationState}
                             onClick={this.onBadgeClick}
                             tabIndex={tabIndex}
diff --git a/src/components/views/rooms/RoomTile.tsx b/src/components/views/rooms/RoomTile.tsx
index ab6562a32cc..e0baf41f191 100644
--- a/src/components/views/rooms/RoomTile.tsx
+++ b/src/components/views/rooms/RoomTile.tsx
@@ -402,11 +402,7 @@ export class RoomTile extends React.PureComponent<ClassProps, State> {
             // aria-hidden because we summarise the unread count/highlight status in a manual aria-label below
             badge = (
                 <div className="mx_RoomTile_badgeContainer" aria-hidden="true">
-                    <NotificationBadge
-                        notification={this.notificationState}
-                        forceCount={false}
-                        roomId={this.props.room.roomId}
-                    />
+                    <NotificationBadge notification={this.notificationState} roomId={this.props.room.roomId} />
                 </div>
             );
         }
diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx
index c0b71709238..315cba3c1cc 100644
--- a/src/components/views/spaces/SpaceTreeLevel.tsx
+++ b/src/components/views/spaces/SpaceTreeLevel.tsx
@@ -113,7 +113,6 @@ export const SpaceButton = <T extends keyof JSX.IntrinsicElements>({
             <div className="mx_SpacePanel_badgeContainer">
                 <NotificationBadge
                     onClick={jumpToNotification}
-                    forceCount={false}
                     notification={notificationState}
                     aria-label={ariaLabel}
                     tabIndex={tabIndex}
diff --git a/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx b/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx
index 098f2d828cf..ddb1dd98d3e 100644
--- a/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx
+++ b/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx
@@ -147,7 +147,7 @@ function ThreadsActivityCentreRow({ room, onClick, notificationLevel }: ThreadsA
             label={room.name}
             Icon={<DecoratedRoomAvatar room={room} size="32px" />}
         >
-            <StatelessNotificationBadge level={notificationLevel} count={0} symbol={null} type="dot" />
+            <StatelessNotificationBadge level={notificationLevel} count={0} symbol={null} forceDot={true} />
         </MenuItem>
     );
 }
diff --git a/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts b/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts
index 115b2085607..72b5380fbd1 100644
--- a/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts
+++ b/src/components/views/spaces/threads-activity-centre/useUnreadThreadRooms.ts
@@ -89,12 +89,12 @@ function computeUnreadThreadRooms(mxClient: MatrixClient, msc3946ProcessDynamicP
     const visibleRooms = mxClient.getVisibleRooms(msc3946ProcessDynamicPredecessor);
 
     let greatestNotificationLevel = NotificationLevel.None;
-    const rooms = [];
+    const rooms: Result["rooms"] = [];
 
     for (const room of visibleRooms) {
         // We only care about rooms with unread threads
         if (VisibilityProvider.instance.isRoomVisible(room) && doesRoomHaveUnreadThreads(room)) {
-            // Get the greatest notification level of all rooms
+            // Get the greatest notification level of all threads
             const notificationLevel = getThreadNotificationLevel(room);
 
             // If the room has an activity notification or less, we ignore it
@@ -110,20 +110,35 @@ function computeUnreadThreadRooms(mxClient: MatrixClient, msc3946ProcessDynamicP
         }
     }
 
-    const sortedRooms = rooms.sort((a, b) => sortRoom(a.notificationLevel, b.notificationLevel));
+    const sortedRooms = rooms.sort((a, b) => sortRoom(a, b));
     return { greatestNotificationLevel, rooms: sortedRooms };
 }
 
+/**
+ * Store the room and its thread notification level
+ */
+type RoomData = Result["rooms"][0];
+
 /**
  * Sort notification level by the most important notification level to the least important
  * Highlight > Notification > Activity
- * @param notificationLevelA - notification level of room A
- * @param notificationLevelB - notification level of room B
+ * If the notification level is the same, we sort by the most recent thread
+ * @param roomDataA - room and notification level of room A
+ * @param roomDataB - room and notification level of room B
  * @returns {number}
  */
-function sortRoom(notificationLevelA: NotificationLevel, notificationLevelB: NotificationLevel): number {
+function sortRoom(roomDataA: RoomData, roomDataB: RoomData): number {
+    const { notificationLevel: notificationLevelA, room: roomA } = roomDataA;
+    const { notificationLevel: notificationLevelB, room: roomB } = roomDataB;
+
+    const timestampA = roomA.getLastThread()?.events.at(-1)?.getTs();
+    const timestampB = roomB.getLastThread()?.events.at(-1)?.getTs();
+
     // NotificationLevel is a numeric enum, so we can compare them directly
     if (notificationLevelA > notificationLevelB) return -1;
     else if (notificationLevelB > notificationLevelA) return 1;
-    else return 0;
+    // Display most recent first
+    else if (!timestampA) return 1;
+    else if (!timestampB) return -1;
+    else return timestampB - timestampA;
 }
diff --git a/test/components/views/rooms/NotificationBadge/StatelessNotificationBadge-test.tsx b/test/components/views/rooms/NotificationBadge/StatelessNotificationBadge-test.tsx
index 66ae273e247..6ee93d82db4 100644
--- a/test/components/views/rooms/NotificationBadge/StatelessNotificationBadge-test.tsx
+++ b/test/components/views/rooms/NotificationBadge/StatelessNotificationBadge-test.tsx
@@ -35,4 +35,23 @@ describe("StatelessNotificationBadge", () => {
         expect(container.querySelector(".mx_NotificationBadge_dot")).not.toBeInTheDocument();
         expect(container.querySelector(".mx_NotificationBadge_knocked")).toBeInTheDocument();
     });
+
+    it("has badge style for notification", () => {
+        const { container } = render(
+            <StatelessNotificationBadge symbol={null} count={3} level={NotificationLevel.Notification} />,
+        );
+        expect(container.querySelector(".mx_NotificationBadge_dot")).not.toBeInTheDocument();
+    });
+
+    it("has dot style for notification when forced", () => {
+        const { container } = render(
+            <StatelessNotificationBadge
+                symbol={null}
+                count={3}
+                level={NotificationLevel.Notification}
+                forceDot={true}
+            />,
+        );
+        expect(container.querySelector(".mx_NotificationBadge_dot")).toBeInTheDocument();
+    });
 });
diff --git a/test/components/views/spaces/ThreadsActivityCentre-test.tsx b/test/components/views/spaces/ThreadsActivityCentre-test.tsx
index f5f183e7c6c..8deb27ec7e4 100644
--- a/test/components/views/spaces/ThreadsActivityCentre-test.tsx
+++ b/test/components/views/spaces/ThreadsActivityCentre-test.tsx
@@ -59,16 +59,23 @@ describe("ThreadsActivityCentre", () => {
     });
     roomWithActivity.name = "Just activity";
 
-    const roomWithNotif = new Room("!room:server", cli, cli.getSafeUserId(), {
+    const roomWithNotif = new Room("!room2:server", cli, cli.getSafeUserId(), {
         pendingEventOrdering: PendingEventOrdering.Detached,
     });
     roomWithNotif.name = "A notification";
 
-    const roomWithHighlight = new Room("!room:server", cli, cli.getSafeUserId(), {
+    const roomWithHighlight = new Room("!room3:server", cli, cli.getSafeUserId(), {
         pendingEventOrdering: PendingEventOrdering.Detached,
     });
     roomWithHighlight.name = "This is a real highlight";
 
+    const getDefaultThreadArgs = (room: Room) => ({
+        room: room,
+        client: cli,
+        authorId: "@foo:bar",
+        participantUserIds: ["@fee:bar"],
+    });
+
     beforeAll(async () => {
         jest.spyOn(MatrixClientPeg, "get").mockReturnValue(cli);
         jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(cli);
@@ -77,26 +84,15 @@ describe("ThreadsActivityCentre", () => {
         jest.spyOn(dmRoomMap, "getUserIdForRoomId");
         jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
 
-        await populateThread({
-            room: roomWithActivity,
-            client: cli,
-            authorId: "@foo:bar",
-            participantUserIds: ["@fee:bar"],
-        });
+        await populateThread(getDefaultThreadArgs(roomWithActivity));
 
-        const notifThreadInfo = await populateThread({
-            room: roomWithNotif,
-            client: cli,
-            authorId: "@foo:bar",
-            participantUserIds: ["@fee:bar"],
-        });
+        const notifThreadInfo = await populateThread(getDefaultThreadArgs(roomWithNotif));
         roomWithNotif.setThreadUnreadNotificationCount(notifThreadInfo.thread.id, NotificationCountType.Total, 1);
 
         const highlightThreadInfo = await populateThread({
-            room: roomWithHighlight,
-            client: cli,
-            authorId: "@foo:bar",
-            participantUserIds: ["@fee:bar"],
+            ...getDefaultThreadArgs(roomWithHighlight),
+            // timestamp
+            ts: 5,
         });
         roomWithHighlight.setThreadUnreadNotificationCount(
             highlightThreadInfo.thread.id,
@@ -181,6 +177,52 @@ describe("ThreadsActivityCentre", () => {
         expect(screen.getByRole("menu")).toMatchSnapshot();
     });
 
+    it("should order the room with the same notification level by most recent", async () => {
+        // Generate two new rooms with threads
+        const secondRoomWithHighlight = new Room("!room4:server", cli, cli.getSafeUserId(), {
+            pendingEventOrdering: PendingEventOrdering.Detached,
+        });
+        secondRoomWithHighlight.name = "This is a second real highlight";
+
+        const secondHighlightThreadInfo = await populateThread({
+            ...getDefaultThreadArgs(secondRoomWithHighlight),
+            // timestamp
+            ts: 1,
+        });
+        secondRoomWithHighlight.setThreadUnreadNotificationCount(
+            secondHighlightThreadInfo.thread.id,
+            NotificationCountType.Highlight,
+            1,
+        );
+
+        const thirdRoomWithHighlight = new Room("!room5:server", cli, cli.getSafeUserId(), {
+            pendingEventOrdering: PendingEventOrdering.Detached,
+        });
+        thirdRoomWithHighlight.name = "This is a third real highlight";
+
+        const thirdHighlightThreadInfo = await populateThread({
+            ...getDefaultThreadArgs(thirdRoomWithHighlight),
+            // timestamp
+            ts: 7,
+        });
+        thirdRoomWithHighlight.setThreadUnreadNotificationCount(
+            thirdHighlightThreadInfo.thread.id,
+            NotificationCountType.Highlight,
+            1,
+        );
+
+        cli.getVisibleRooms = jest
+            .fn()
+            .mockReturnValue([roomWithHighlight, secondRoomWithHighlight, thirdRoomWithHighlight]);
+
+        renderTAC();
+        await userEvent.click(getTACButton());
+
+        // The room should be ordered by the most recent thread
+        // thirdHighlightThreadInfo (timestamp 7) > highlightThreadInfo (timestamp 5) > secondHighlightThreadInfo (timestamp 1)
+        expect(screen.getByRole("menu")).toMatchSnapshot();
+    });
+
     it("should block Ctrl/CMD + k shortcut", async () => {
         cli.getVisibleRooms = jest.fn().mockReturnValue([roomWithHighlight]);
 
diff --git a/test/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap b/test/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap
index 5b9e2091b75..0d2841c6148 100644
--- a/test/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap
+++ b/test/components/views/spaces/__snapshots__/ThreadsActivityCentre-test.tsx.snap
@@ -38,7 +38,7 @@ exports[`ThreadsActivityCentre renders notifications matching the snapshot 1`] =
       >
         <span
           class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
-          data-color="6"
+          data-color="3"
           data-testid="avatar-img"
           data-type="round"
           role="presentation"
@@ -86,7 +86,7 @@ exports[`ThreadsActivityCentre renders notifications matching the snapshot 1`] =
       >
         <span
           class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
-          data-color="6"
+          data-color="2"
           data-testid="avatar-img"
           data-type="round"
           role="presentation"
@@ -158,3 +158,176 @@ exports[`ThreadsActivityCentre should match snapshot when empty 1`] = `
   </div>
 </div>
 `;
+
+exports[`ThreadsActivityCentre should order the room with the same notification level by most recent 1`] = `
+<div
+  aria-labelledby="radix-31"
+  aria-orientation="vertical"
+  class="_menu_1x5h1_17"
+  data-align="end"
+  data-orientation="vertical"
+  data-radix-menu-content=""
+  data-side="right"
+  data-state="open"
+  dir="ltr"
+  id="radix-32"
+  role="menu"
+  style="outline: none; --radix-dropdown-menu-content-transform-origin: var(--radix-popper-transform-origin); --radix-dropdown-menu-content-available-width: var(--radix-popper-available-width); --radix-dropdown-menu-content-available-height: var(--radix-popper-available-height); --radix-dropdown-menu-trigger-width: var(--radix-popper-anchor-width); --radix-dropdown-menu-trigger-height: var(--radix-popper-anchor-height); pointer-events: auto;"
+  tabindex="-1"
+>
+  <h3
+    class="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 _title_1x5h1_83"
+    id=":r8:"
+  >
+    Threads activity
+  </h3>
+  <div
+    class="mx_ThreadsActivityCentre_rows"
+  >
+    <button
+      class="mx_ThreadsActivityCentreRow _item_1gwvj_17 _interactive_1gwvj_36"
+      data-kind="primary"
+      data-orientation="vertical"
+      data-radix-collection-item=""
+      role="menuitem"
+      tabindex="-1"
+    >
+      <div
+        class="_icon_1gwvj_44"
+      >
+        <span
+          class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
+          data-color="5"
+          data-testid="avatar-img"
+          data-type="round"
+          role="presentation"
+          style="--cpd-avatar-size: 32px;"
+        >
+          T
+        </span>
+      </div>
+      <span
+        class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
+      >
+        This is a third real highlight
+      </span>
+      <svg
+        aria-hidden="true"
+        class="_nav-hint_1gwvj_60"
+        fill="currentColor"
+        height="24"
+        viewBox="8 0 8 24"
+        width="8"
+        xmlns="http://www.w3.org/2000/svg"
+      >
+        <path
+          d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
+        />
+      </svg>
+      <div
+        class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_dot"
+      >
+        <span
+          class="mx_NotificationBadge_count"
+        />
+      </div>
+    </button>
+    <button
+      class="mx_ThreadsActivityCentreRow _item_1gwvj_17 _interactive_1gwvj_36"
+      data-kind="primary"
+      data-orientation="vertical"
+      data-radix-collection-item=""
+      role="menuitem"
+      tabindex="-1"
+    >
+      <div
+        class="_icon_1gwvj_44"
+      >
+        <span
+          class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
+          data-color="3"
+          data-testid="avatar-img"
+          data-type="round"
+          role="presentation"
+          style="--cpd-avatar-size: 32px;"
+        >
+          T
+        </span>
+      </div>
+      <span
+        class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
+      >
+        This is a real highlight
+      </span>
+      <svg
+        aria-hidden="true"
+        class="_nav-hint_1gwvj_60"
+        fill="currentColor"
+        height="24"
+        viewBox="8 0 8 24"
+        width="8"
+        xmlns="http://www.w3.org/2000/svg"
+      >
+        <path
+          d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
+        />
+      </svg>
+      <div
+        class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_dot"
+      >
+        <span
+          class="mx_NotificationBadge_count"
+        />
+      </div>
+    </button>
+    <button
+      class="mx_ThreadsActivityCentreRow _item_1gwvj_17 _interactive_1gwvj_36"
+      data-kind="primary"
+      data-orientation="vertical"
+      data-radix-collection-item=""
+      role="menuitem"
+      tabindex="-1"
+    >
+      <div
+        class="_icon_1gwvj_44"
+      >
+        <span
+          class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
+          data-color="4"
+          data-testid="avatar-img"
+          data-type="round"
+          role="presentation"
+          style="--cpd-avatar-size: 32px;"
+        >
+          T
+        </span>
+      </div>
+      <span
+        class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_1gwvj_53"
+      >
+        This is a second real highlight
+      </span>
+      <svg
+        aria-hidden="true"
+        class="_nav-hint_1gwvj_60"
+        fill="currentColor"
+        height="24"
+        viewBox="8 0 8 24"
+        width="8"
+        xmlns="http://www.w3.org/2000/svg"
+      >
+        <path
+          d="M8.7 17.3a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7l3.9-3.9-3.9-3.9a.948.948 0 0 1-.275-.7.95.95 0 0 1 .275-.7.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275l4.6 4.6c.1.1.17.208.213.325.041.117.062.242.062.375s-.02.258-.063.375a.876.876 0 0 1-.212.325l-4.6 4.6a.948.948 0 0 1-.7.275.948.948 0 0 1-.7-.275Z"
+        />
+      </svg>
+      <div
+        class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_level_highlight mx_NotificationBadge_dot"
+      >
+        <span
+          class="mx_NotificationBadge_count"
+        />
+      </div>
+    </button>
+  </div>
+</div>
+`;
diff --git a/yarn.lock b/yarn.lock
index ce60176a7a5..6bcc5013577 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3013,7 +3013,7 @@
   resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
   integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
 
-"@vector-im/compound-design-tokens@^1.0.0":
+"@vector-im/compound-design-tokens@^1.2.0":
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.2.0.tgz#ccb15fffc24cc70d83593bfc5348e6a0198cc08a"
   integrity sha512-8LSbb38KxvStcOQZDSi7lI4oqtCuHFEgEQi9Q0KUx+5OnklfdyJ638txM1bznX/Cp9lHgMk4dHrTiQHBOE0ZuA==
@@ -6860,7 +6860,7 @@ matrix-events-sdk@0.0.1:
 
 "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
   version "31.5.0"
-  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/461aeae2815a223c817c9768e26220cec4a69d12"
+  resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/8e0ef5ff2cd927efa1bd22cabb075a14b10e39d5"
   dependencies:
     "@babel/runtime" "^7.12.5"
     "@matrix-org/matrix-sdk-crypto-wasm" "^4.6.0"