mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
fix(app): stop the inbox reshuffling unread channels as you read them (#1894)
This commit is contained in:
1 parent
d8bad50eec
commit
b81bfc80fd
4 files changed
+168
-91
No files matched your search
@@ -1,49 +1,34 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import {describe, expect, it} from 'vitest';
|
import {describe, expect, it} from 'vitest';
|
||||||
import {mergeFrozenUnreadOrder} from './UnreadChannelOrder';
|
import {selectFrozenUnreadChannels} from './UnreadChannelOrder';
|
||||||
|
|
||||||
function ids(channels: ReadonlyArray<{id: string}>): Array<string> {
|
function ids(channels: ReadonlyArray<{id: string}>): Array<string> {
|
||||||
return channels.map((channel) => channel.id);
|
return channels.map((channel) => channel.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('mergeFrozenUnreadOrder', () => {
|
describe('selectFrozenUnreadChannels', () => {
|
||||||
it('keeps channels in their frozen position while the list is open', () => {
|
it('keeps channels in their frozen position while the list is open', () => {
|
||||||
const order = new Map([
|
const order = new Map([
|
||||||
['a', 0],
|
['a', 0],
|
||||||
['b', 1],
|
['b', 1],
|
||||||
['c', 2],
|
['c', 2],
|
||||||
]);
|
]);
|
||||||
const merged = mergeFrozenUnreadOrder(order, [{id: 'c'}, {id: 'a'}, {id: 'b'}]);
|
expect(ids(selectFrozenUnreadChannels(order, [{id: 'c'}, {id: 'a'}, {id: 'b'}]))).toEqual(['a', 'b', 'c']);
|
||||||
expect(ids(merged)).toEqual(['a', 'b', 'c']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a channel that becomes unread after the order was frozen', () => {
|
it('hides a channel that becomes unread after the order was frozen', () => {
|
||||||
const order = new Map([['a', 0]]);
|
const order = new Map([['a', 0]]);
|
||||||
const merged = mergeFrozenUnreadOrder(order, [{id: 'a'}, {id: 'late'}]);
|
expect(ids(selectFrozenUnreadChannels(order, [{id: 'a'}, {id: 'late'}]))).toEqual(['a']);
|
||||||
expect(ids(merged)).toEqual(['a', 'late']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('appends late arrivals after existing entries rather than reshuffling', () => {
|
it('never grows the list when every live channel is new to the order', () => {
|
||||||
const order = new Map([
|
|
||||||
['a', 0],
|
|
||||||
['b', 1],
|
|
||||||
]);
|
|
||||||
const merged = mergeFrozenUnreadOrder(order, [{id: 'late'}, {id: 'b'}, {id: 'a'}]);
|
|
||||||
expect(ids(merged)).toEqual(['a', 'b', 'late']);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('remembers a late arrival position once assigned', () => {
|
|
||||||
const order = new Map([['a', 0]]);
|
const order = new Map([['a', 0]]);
|
||||||
mergeFrozenUnreadOrder(order, [{id: 'a'}, {id: 'late'}]);
|
expect(ids(selectFrozenUnreadChannels(order, [{id: 'x'}, {id: 'y'}, {id: 'z'}]))).toEqual([]);
|
||||||
const merged = mergeFrozenUnreadOrder(order, [{id: 'late'}, {id: 'a'}]);
|
|
||||||
expect(ids(merged)).toEqual(['a', 'late']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('drops nothing when every channel is new', () => {
|
it('yields nothing for an empty order', () => {
|
||||||
const order = new Map<string, number>();
|
expect(ids(selectFrozenUnreadChannels(new Map<string, number>(), [{id: 'x'}]))).toEqual([]);
|
||||||
const merged = mergeFrozenUnreadOrder(order, [{id: 'x'}, {id: 'y'}, {id: 'z'}]);
|
|
||||||
expect(ids(merged)).toEqual(['x', 'y', 'z']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not resurrect channels that are no longer unread', () => {
|
it('does not resurrect channels that are no longer unread', () => {
|
||||||
@@ -51,7 +36,12 @@ describe('mergeFrozenUnreadOrder', () => {
|
|||||||
['a', 0],
|
['a', 0],
|
||||||
['gone', 1],
|
['gone', 1],
|
||||||
]);
|
]);
|
||||||
const merged = mergeFrozenUnreadOrder(order, [{id: 'a'}]);
|
expect(ids(selectFrozenUnreadChannels(order, [{id: 'a'}]))).toEqual(['a']);
|
||||||
expect(ids(merged)).toEqual(['a']);
|
});
|
||||||
|
|
||||||
|
it('does not mutate the order it was given', () => {
|
||||||
|
const order = new Map([['a', 0]]);
|
||||||
|
selectFrozenUnreadChannels(order, [{id: 'a'}, {id: 'late'}]);
|
||||||
|
expect([...order.keys()]).toEqual(['a']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,21 +1,62 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
function resolveUnreadOrderIndex(order: Map<string, number>, channelId: string): number {
|
import {useEffect, useMemo, useRef, useState} from 'react';
|
||||||
const index = order.get(channelId);
|
|
||||||
if (index == null) {
|
export const UNREAD_REFREEZE_COOLDOWN_MS = 10_000;
|
||||||
return Number.POSITIVE_INFINITY;
|
|
||||||
}
|
export interface FrozenUnreadOrder {
|
||||||
return index;
|
order: Map<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeFrozenUnreadOrder<T extends {id: string}>(
|
export function selectFrozenUnreadChannels<T extends {id: string}>(
|
||||||
order: Map<string, number>,
|
order: ReadonlyMap<string, number>,
|
||||||
current: ReadonlyArray<T>,
|
live: ReadonlyArray<T>,
|
||||||
): Array<T> {
|
): Array<T> {
|
||||||
for (const channel of current) {
|
const held: Array<{index: number; channel: T}> = [];
|
||||||
if (!order.has(channel.id)) {
|
for (const channel of live) {
|
||||||
order.set(channel.id, order.size);
|
const index = order.get(channel.id);
|
||||||
}
|
if (index == null) continue;
|
||||||
|
held.push({index, channel});
|
||||||
}
|
}
|
||||||
return [...current].sort((a, b) => resolveUnreadOrderIndex(order, a.id) - resolveUnreadOrderIndex(order, b.id));
|
held.sort((a, b) => a.index - b.index);
|
||||||
|
return held.map((entry) => entry.channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFrozenUnreadOrder<S extends FrozenUnreadOrder, T extends {id: string}>(
|
||||||
|
takeSnapshot: () => S,
|
||||||
|
live: ReadonlyArray<T>,
|
||||||
|
): {snapshot: S; channels: Array<T>} {
|
||||||
|
const [snapshot, setSnapshot] = useState(takeSnapshot);
|
||||||
|
const [cooldownAttempt, setCooldownAttempt] = useState(0);
|
||||||
|
const snapshotTakenAtRef = useRef(Date.now());
|
||||||
|
const hasShownFrozenListRef = useRef(false);
|
||||||
|
const channels = useMemo(() => selectFrozenUnreadChannels(snapshot.order, live), [snapshot, live]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (channels.length > 0) {
|
||||||
|
hasShownFrozenListRef.current = true;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const next = takeSnapshot();
|
||||||
|
if (next.order.size === 0) {
|
||||||
|
if (snapshot.order.size > 0) {
|
||||||
|
setSnapshot(next);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (hasShownFrozenListRef.current) {
|
||||||
|
const heldFor = Date.now() - snapshotTakenAtRef.current;
|
||||||
|
if (heldFor < UNREAD_REFREEZE_COOLDOWN_MS) {
|
||||||
|
const timer = setTimeout(
|
||||||
|
() => setCooldownAttempt((attempt) => attempt + 1),
|
||||||
|
UNREAD_REFREEZE_COOLDOWN_MS - heldFor,
|
||||||
|
);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snapshotTakenAtRef.current = Date.now();
|
||||||
|
hasShownFrozenListRef.current = false;
|
||||||
|
setSnapshot(next);
|
||||||
|
return undefined;
|
||||||
|
}, [channels, snapshot, cooldownAttempt]);
|
||||||
|
return {snapshot, channels};
|
||||||
}
|
}
|
||||||
@@ -9,12 +9,20 @@
|
|||||||
--inbox-icon-button-radius: 0.375rem;
|
--inbox-icon-button-radius: 0.375rem;
|
||||||
--inbox-collapse-button-size: 1.375rem;
|
--inbox-collapse-button-size: 1.375rem;
|
||||||
--inbox-collapse-button-radius: 0.375rem;
|
--inbox-collapse-button-radius: 0.375rem;
|
||||||
|
--inbox-sticky-header-height: 3.5rem;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
padding: 0 var(--inbox-scroller-padding-x) var(--inbox-scroller-padding-bottom);
|
padding: 0 var(--inbox-scroller-padding-x) var(--inbox-scroller-padding-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stickyChannelHeader {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background-color: var(--form-surface-background);
|
||||||
|
}
|
||||||
|
|
||||||
.guildGroup {
|
.guildGroup {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@@ -100,6 +108,8 @@
|
|||||||
|
|
||||||
.messageRow {
|
.messageRow {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
isolation: isolate;
|
||||||
|
scroll-margin-top: var(--inbox-sticky-header-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
.messageActions {
|
.messageActions {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
import Accessibility from '@app/features/accessibility/state/Accessibility';
|
import Accessibility from '@app/features/accessibility/state/Accessibility';
|
||||||
import {mergeFrozenUnreadOrder} from '@app/features/app/components/floating/UnreadChannelOrder';
|
import {useFrozenUnreadOrder} from '@app/features/app/components/floating/UnreadChannelOrder';
|
||||||
import styles from '@app/features/app/components/floating/UnreadChannelsContent.module.css';
|
import styles from '@app/features/app/components/floating/UnreadChannelsContent.module.css';
|
||||||
import {
|
import {
|
||||||
BULK_PREVIEW_CHANNEL_BATCH_SIZE,
|
BULK_PREVIEW_CHANNEL_BATCH_SIZE,
|
||||||
@@ -27,6 +27,7 @@ import {ensureMembersForMessages} from '@app/features/messaging/commands/Message
|
|||||||
import {InboxMessageHeader} from '@app/features/messaging/components/popouts/InboxMessageHeader';
|
import {InboxMessageHeader} from '@app/features/messaging/components/popouts/InboxMessageHeader';
|
||||||
import {useMessageListKeyboardNavigation} from '@app/features/messaging/hooks/useMessageListKeyboardNavigation';
|
import {useMessageListKeyboardNavigation} from '@app/features/messaging/hooks/useMessageListKeyboardNavigation';
|
||||||
import {useMessageSelectionCopyForMessages} from '@app/features/messaging/hooks/useMessageSelectionCopy';
|
import {useMessageSelectionCopyForMessages} from '@app/features/messaging/hooks/useMessageSelectionCopy';
|
||||||
|
import {NearViewportSurfaceContext} from '@app/features/messaging/hooks/useNearViewport';
|
||||||
import {Message} from '@app/features/messaging/models/MessagingMessage';
|
import {Message} from '@app/features/messaging/models/MessagingMessage';
|
||||||
import {ChannelMessages} from '@app/features/messaging/state/ChannelMessages';
|
import {ChannelMessages} from '@app/features/messaging/state/ChannelMessages';
|
||||||
import {createChannelStream} from '@app/features/messaging/utils/MessageGroupingUtils';
|
import {createChannelStream} from '@app/features/messaging/utils/MessageGroupingUtils';
|
||||||
@@ -136,6 +137,29 @@ interface ChannelPreviewData {
|
|||||||
const INITIAL_VISIBLE_CHANNELS = 10;
|
const INITIAL_VISIBLE_CHANNELS = 10;
|
||||||
const LOAD_MORE_CHUNK = 10;
|
const LOAD_MORE_CHUNK = 10;
|
||||||
|
|
||||||
|
interface FrozenUnreadCounts {
|
||||||
|
mentionCount: number;
|
||||||
|
unreadCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FrozenUnreadChannels {
|
||||||
|
order: Map<string, number>;
|
||||||
|
counts: Map<string, FrozenUnreadCounts>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function freezeUnreadChannels(): FrozenUnreadChannels {
|
||||||
|
const order = new Map<string, number>();
|
||||||
|
const counts = new Map<string, FrozenUnreadCounts>();
|
||||||
|
for (const [index, channel] of getUnreadChannels().entries()) {
|
||||||
|
order.set(channel.id, index);
|
||||||
|
counts.set(channel.id, {
|
||||||
|
mentionCount: ReadStates.getMentionCount(channel.id),
|
||||||
|
unreadCount: ReadStates.getUnreadCount(channel.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {order, counts};
|
||||||
|
}
|
||||||
|
|
||||||
interface CacheEntry {
|
interface CacheEntry {
|
||||||
cacheKey: string;
|
cacheKey: string;
|
||||||
data: ChannelPreviewData;
|
data: ChannelPreviewData;
|
||||||
@@ -350,22 +374,24 @@ const UnreadChannelCard = observer(function UnreadChannelCard({
|
|||||||
channel,
|
channel,
|
||||||
headingId,
|
headingId,
|
||||||
previewData,
|
previewData,
|
||||||
|
mentionCount,
|
||||||
|
unreadCount,
|
||||||
}: {
|
}: {
|
||||||
channel: Channel;
|
channel: Channel;
|
||||||
headingId: string;
|
headingId: string;
|
||||||
previewData: ChannelPreviewData | null;
|
previewData: ChannelPreviewData | null;
|
||||||
|
mentionCount: number;
|
||||||
|
unreadCount: number;
|
||||||
}) {
|
}) {
|
||||||
const {i18n} = useLingui();
|
const {i18n} = useLingui();
|
||||||
const messageDisplayCompact = UserSettings.getMessageDisplayCompact();
|
const messageDisplayCompact = UserSettings.getMessageDisplayCompact();
|
||||||
const messageGroupSpacing = Accessibility.getMessageGroupSpacingValue(messageDisplayCompact);
|
const messageGroupSpacing = Accessibility.getMessageGroupSpacingValue(messageDisplayCompact);
|
||||||
const mentionCount = ReadStates.getMentionCount(channel.id);
|
|
||||||
const isCollapsed = UnreadChannels.isCollapsed(channel.id);
|
const isCollapsed = UnreadChannels.isCollapsed(channel.id);
|
||||||
const isMuted = UserGuildSettings.isMutedAtAnyLevel(channel.guildId ?? null, channel.id);
|
const isMuted = UserGuildSettings.isMutedAtAnyLevel(channel.guildId ?? null, channel.id);
|
||||||
const previewMessages = previewData?.messages ?? [];
|
const previewMessages = previewData?.messages ?? [];
|
||||||
const oldestUnreadMessageId = previewData?.oldestUnreadMessageId ?? null;
|
const oldestUnreadMessageId = previewData?.oldestUnreadMessageId ?? null;
|
||||||
const totalUnreadCount = ReadStates.getUnreadCount(channel.id);
|
|
||||||
const hasMoreUnreadThanShown =
|
const hasMoreUnreadThanShown =
|
||||||
oldestUnreadMessageId != null && totalUnreadCount > 0 && totalUnreadCount > previewMessages.length;
|
oldestUnreadMessageId != null && unreadCount > 0 && unreadCount > previewMessages.length;
|
||||||
const spammerOverrideVersion = LocalUserSpamOverride.version;
|
const spammerOverrideVersion = LocalUserSpamOverride.version;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (previewMessages.length === 0) return;
|
if (previewMessages.length === 0) return;
|
||||||
@@ -497,6 +523,7 @@ const UnreadChannelCard = observer(function UnreadChannelCard({
|
|||||||
</h2>
|
</h2>
|
||||||
<InboxMessageHeader
|
<InboxMessageHeader
|
||||||
channel={channel}
|
channel={channel}
|
||||||
|
className={styles.stickyChannelHeader}
|
||||||
onClick={handleHeaderClick}
|
onClick={handleHeaderClick}
|
||||||
mentionCount={mentionCount}
|
mentionCount={mentionCount}
|
||||||
leftAdornment={
|
leftAdornment={
|
||||||
@@ -648,19 +675,14 @@ const UnreadChannelCard = observer(function UnreadChannelCard({
|
|||||||
export const UnreadChannelsContent = observer(function UnreadChannelsContent() {
|
export const UnreadChannelsContent = observer(function UnreadChannelsContent() {
|
||||||
const {i18n} = useLingui();
|
const {i18n} = useLingui();
|
||||||
const scrollerRef = useRef<ScrollerHandle | null>(null);
|
const scrollerRef = useRef<ScrollerHandle | null>(null);
|
||||||
|
const resolveInboxScrollSurface = useMemo(() => () => scrollerRef.current?.getViewportElement() ?? null, []);
|
||||||
const readStateVersion = ReadStates.version;
|
const readStateVersion = ReadStates.version;
|
||||||
const settingsVersion = UserGuildSettings.version;
|
const settingsVersion = UserGuildSettings.version;
|
||||||
const frozenOrderRef = useRef<Map<string, number> | null>(null);
|
const liveUnreadChannels = useMemo(() => getUnreadChannels(), [readStateVersion, settingsVersion]);
|
||||||
if (frozenOrderRef.current === null) {
|
const {snapshot: frozenUnread, channels: allUnreadChannels} = useFrozenUnreadOrder(
|
||||||
frozenOrderRef.current = new Map(getUnreadChannels().map((channel, index) => [channel.id, index]));
|
freezeUnreadChannels,
|
||||||
}
|
liveUnreadChannels,
|
||||||
const allUnreadChannels = useMemo(() => {
|
);
|
||||||
const order = frozenOrderRef.current;
|
|
||||||
if (order == null) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return mergeFrozenUnreadOrder(order, getUnreadChannels());
|
|
||||||
}, [readStateVersion, settingsVersion]);
|
|
||||||
const [loadedCount, setLoadedCount] = useState(INITIAL_VISIBLE_CHANNELS);
|
const [loadedCount, setLoadedCount] = useState(INITIAL_VISIBLE_CHANNELS);
|
||||||
const visibleChannels = useMemo(() => allUnreadChannels.slice(0, loadedCount), [allUnreadChannels, loadedCount]);
|
const visibleChannels = useMemo(() => allUnreadChannels.slice(0, loadedCount), [allUnreadChannels, loadedCount]);
|
||||||
const channelPreviews = useBulkChannelPreviews(visibleChannels);
|
const channelPreviews = useBulkChannelPreviews(visibleChannels);
|
||||||
@@ -697,19 +719,29 @@ export const UnreadChannelsContent = observer(function UnreadChannelsContent() {
|
|||||||
return groups;
|
return groups;
|
||||||
}, [visibleChannels, channelPreviews, i18n.locale]);
|
}, [visibleChannels, channelPreviews, i18n.locale]);
|
||||||
const unreadRows = useMemo(() => {
|
const unreadRows = useMemo(() => {
|
||||||
const rows: Array<{channel: Channel; endsGroup: boolean; groupLabel: string; key: string}> = [];
|
const rows: Array<{
|
||||||
|
channel: Channel;
|
||||||
|
endsGroup: boolean;
|
||||||
|
groupLabel: string;
|
||||||
|
key: string;
|
||||||
|
mentionCount: number;
|
||||||
|
unreadCount: number;
|
||||||
|
}> = [];
|
||||||
for (const group of groupedChannels) {
|
for (const group of groupedChannels) {
|
||||||
for (const [index, channel] of group.channels.entries()) {
|
for (const [index, channel] of group.channels.entries()) {
|
||||||
|
const counts = frozenUnread.counts.get(channel.id);
|
||||||
rows.push({
|
rows.push({
|
||||||
channel,
|
channel,
|
||||||
endsGroup: index === group.channels.length - 1,
|
endsGroup: index === group.channels.length - 1,
|
||||||
groupLabel: group.label,
|
groupLabel: group.label,
|
||||||
key: channel.id,
|
key: channel.id,
|
||||||
|
mentionCount: counts?.mentionCount ?? 0,
|
||||||
|
unreadCount: counts?.unreadCount ?? 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
}, [groupedChannels]);
|
}, [groupedChannels, frozenUnread]);
|
||||||
const hasMore = loadedCount < allUnreadChannels.length;
|
const hasMore = loadedCount < allUnreadChannels.length;
|
||||||
const handleScroll = useCallback(
|
const handleScroll = useCallback(
|
||||||
(event: React.UIEvent<HTMLDivElement>) => {
|
(event: React.UIEvent<HTMLDivElement>) => {
|
||||||
@@ -780,40 +812,44 @@ export const UnreadChannelsContent = observer(function UnreadChannelsContent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Scroller
|
<NearViewportSurfaceContext.Provider value={resolveInboxScrollSurface}>
|
||||||
key="unread-channels-scroller"
|
<Scroller
|
||||||
className={styles.scroller}
|
key="unread-channels-scroller"
|
||||||
ref={scrollerRef}
|
className={styles.scroller}
|
||||||
onScroll={handleScroll}
|
ref={scrollerRef}
|
||||||
onCopy={onCopySelectedMessages}
|
onScroll={handleScroll}
|
||||||
data-message-selection-root="true"
|
onCopy={onCopySelectedMessages}
|
||||||
data-flx="app.floating.unread-channels-content.scroller"
|
data-message-selection-root="true"
|
||||||
>
|
data-flx="app.floating.unread-channels-content.scroller"
|
||||||
<div className={styles.list} role="list" data-flx="app.floating.unread-channels-content.list">
|
>
|
||||||
{unreadRows.map((row, rowIndex) => (
|
<div className={styles.list} role="list" data-flx="app.floating.unread-channels-content.list">
|
||||||
<div
|
{unreadRows.map((row, rowIndex) => (
|
||||||
key={row.key}
|
<div
|
||||||
className={clsx(styles.row, row.endsGroup && styles.rowGroupEnd)}
|
key={row.key}
|
||||||
role="listitem"
|
className={clsx(styles.row, row.endsGroup && styles.rowGroupEnd)}
|
||||||
aria-posinset={rowIndex + 1}
|
role="listitem"
|
||||||
aria-setsize={unreadRows.length}
|
aria-posinset={rowIndex + 1}
|
||||||
data-flx="app.floating.unread-channels-content.row"
|
aria-setsize={unreadRows.length}
|
||||||
>
|
data-flx="app.floating.unread-channels-content.row"
|
||||||
<section
|
|
||||||
className={styles.guildGroup}
|
|
||||||
aria-label={row.groupLabel}
|
|
||||||
data-flx="app.floating.unread-channels-content.guild-group"
|
|
||||||
>
|
>
|
||||||
<UnreadChannelCard
|
<section
|
||||||
channel={row.channel}
|
className={styles.guildGroup}
|
||||||
headingId={`inbox-unread-channel-${row.channel.id}`}
|
aria-label={row.groupLabel}
|
||||||
previewData={channelPreviews.get(row.channel.id) ?? null}
|
data-flx="app.floating.unread-channels-content.guild-group"
|
||||||
data-flx="app.floating.unread-channels-content.unread-channel-card"
|
>
|
||||||
/>
|
<UnreadChannelCard
|
||||||
</section>
|
channel={row.channel}
|
||||||
</div>
|
headingId={`inbox-unread-channel-${row.channel.id}`}
|
||||||
))}
|
previewData={channelPreviews.get(row.channel.id) ?? null}
|
||||||
</div>
|
mentionCount={row.mentionCount}
|
||||||
</Scroller>
|
unreadCount={row.unreadCount}
|
||||||
|
data-flx="app.floating.unread-channels-content.unread-channel-card"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Scroller>
|
||||||
|
</NearViewportSurfaceContext.Provider>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user