Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/architecture-audit-2026-07-23/TeamInbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

- TypeScript `tsc --noEmit`: passed.
- Tauri application `cargo check -p org2`: passed.
- Focused Rust Team Inbox tests: 7 passed.
- Focused Rust Team Inbox tests: 12 passed.

## Layer 2 — Dead code and structural deduplication

Expand Down
4 changes: 2 additions & 2 deletions docs/frontend-ui-audit-2026-07-23/TeamInbox.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Frontend UI Audit — Team Inbox

**Files:** `src/modules/MainApp/TeamInbox/**/*.tsx`
**Date:** 2026-07-23
**Files:** `src/modules/MainApp/TeamInbox/**/*.tsx`
**Date:** 2026-07-23
**Auditor:** ORGII implementation session

## D1 — Raw HTML vs Design System
Expand Down
28 changes: 28 additions & 0 deletions docs/frontend-ui-audit-2026-07-24/TeamInboxReactLifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Team Inbox React Lifecycle — Frontend UI Audit

## Scope

- `src/modules/MainApp/TeamInbox/components/TeamInboxList.tsx`
- `src/modules/MainApp/TeamInbox/TeamInboxView.tsx`
- `src/modules/MainApp/TeamInbox/useTeamInboxWorkItemBody.ts`

## Summary

| Verdict | Count |
| ---------------- | ----: |
| fix | 2 |
| keep with reason | 4 |
| abstract | 0 |

No cross-file design-system sweep candidate was found.

## Findings

| Line | Element | Verdict | Reason | Suggested change |
| -------------------------------: | ------------------------------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `TeamInboxList.tsx:87` | Recency grouping | fix | Calling `Date.now()` during render violates React render purity and makes the memo depend on an untracked value. | Pass the load-time reference timestamp from the owning view and include it in the memo dependencies. |
| `useTeamInboxWorkItemBody.ts:38` | Selected Work Item body effect | fix | Synchronously resetting state inside the effect causes an extra render and trips the React lifecycle rule. | Tag resolved state with the request key and derive the loading fallback during render when keys differ. |
| `TeamInboxView.tsx:64` | Page load lifecycle | keep with reason | The effect owns one abort controller per request, checks cancellation before state writes, and aborts on dependency change or unmount. | Keep. |
| `TeamInboxView.tsx:313` | Loading/error/empty states | keep with reason | The shared `Placeholder` component expresses all three states consistently with the rest of the application. | Keep. |
| `TeamInboxView.tsx:328` | Split list/detail composition | keep with reason | `SplitViewLayout` and `TeamInboxList` are existing shared layout and feature boundaries; another wrapper would add indirection. | Keep. |
| `TeamInboxList.tsx:92` | Filter tabs and unread badges | keep with reason | The implementation uses the shared `TabPill` control, translated labels, and explicit accessible badge labels. | Keep. |
3 changes: 2 additions & 1 deletion src/features/Org2Cloud/useOrg2CloudRealtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,7 @@ export function useOrg2CloudRealtime(): void {

const unsubscribes: Array<() => void> = [];
const orgId = activeRealtimeOrgId;
const orgTeardownAt = orgTeardownAtRef.current;
if (!broadcastSignals) {
unsubscribes.push(
connection.subscribe({
Expand Down Expand Up @@ -642,7 +643,7 @@ export function useOrg2CloudRealtime(): void {

return () => {
for (const unsub of unsubscribes) unsub();
orgTeardownAtRef.current.set(orgId, Date.now());
orgTeardownAt.set(orgId, Date.now());
setRosterRealtimeConnected((current) => {
if (!(orgId in current)) return current;
const next = { ...current };
Expand Down
95 changes: 95 additions & 0 deletions src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1038,6 +1038,101 @@
"markAllAsRead": "Mark all as read",
"scrollToUnread": "Go to unread"
},
"teamInbox": {
"title": "Team Inbox",
"listLabel": "Team Inbox list",
"itemsLabel": "Team Inbox items",
"unreadCount": "{{count}} unread",
"allRead": "All caught up",
"loadMore": "Load more",
"filters": {
"all": "All",
"mentions": "Mentions",
"assigned": "Assigned"
},
"status": {
"read": "Read",
"unread": "Unread"
},
"row": {
"assignedSummary": "{{status}} · {{priority}}"
},
"search": {
"placeholder": "Search inbox",
"ariaLabel": "Search Team Inbox"
},
"groups": {
"today": "Today",
"yesterday": "Yesterday",
"thisWeek": "This week",
"earlier": "Earlier"
},
"empty": {
"title": "Nothing here yet",
"subtitle": "Mentions and assigned work items will appear here.",
"selectTitle": "Select an item",
"selectSubtitle": "View its comment context or work item details.",
"mentions": {
"title": "No mentions",
"subtitle": "When a teammate @mentions you in a comment, it shows up here."
},
"assigned": {
"title": "Nothing assigned to you",
"subtitle": "Work items assigned to you will appear here."
},
"noResults": {
"title": "No matches",
"subtitle": "No items match “{{query}}”."
}
},
"loading": "Loading Team Inbox…",
"loadMore": "Load more",
"errors": {
"loadTitle": "Unable to load Team Inbox",
"load": "Unable to load Team Inbox",
"refresh": "Unable to refresh Team Inbox",
"markRead": "Unable to mark this item as read. Try again.",
"markUnread": "Unable to mark this item as unread. Try again.",
"markAllRead": "Unable to mark all items as read. Try again."
},
"detail": {
"assignedSubtitle": "Assigned work item",
"mentionSubtitle": "Mentioned in a comment",
"mentionedYou": "mentioned you"
},
"actions": {
"markRead": "Mark as read",
"markUnread": "Mark as unread",
"openWorkItem": "Open work item",
"openSession": "Open session"
},
"fields": {
"status": "Status",
"priority": "Priority",
"assignee": "Assignee",
"workItemId": "Work item ID",
"session": "Session",
"comments": "Comments",
"threadId": "Thread ID",
"commentId": "Comment ID"
},
"workItemStatus": {
"backlog": "Backlog",
"todo": "To do",
"in_progress": "In Progress",
"in_review": "In Review",
"blocked": "Blocked",
"done": "Done",
"cancelled": "Cancelled"
},
"priority": {
"none": "No priority",
"low": "Low",
"medium": "Medium",
"high": "High",
"urgent": "Urgent"
}
},
"placeholderTypes": {
"mcpServer": "MCP server",
"connection": "connection",
Expand Down
95 changes: 95 additions & 0 deletions src/i18n/locales/zh/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,101 @@
"markAllAsRead": "全部标记为已读",
"scrollToUnread": "跳转到未读"
},
"teamInbox": {
"title": "团队收件箱",
"listLabel": "团队收件箱列表",
"itemsLabel": "团队收件箱事项",
"unreadCount": "{{count}} 条未读",
"allRead": "已全部阅读",
"loadMore": "加载更多",
"filters": {
"all": "全部",
"mentions": "提及",
"assigned": "分配给我"
},
"status": {
"read": "已读",
"unread": "未读"
},
"row": {
"assignedSummary": "{{status}} · {{priority}}"
},
"search": {
"placeholder": "搜索收件箱",
"ariaLabel": "搜索团队收件箱"
},
"groups": {
"today": "今天",
"yesterday": "昨天",
"thisWeek": "本周",
"earlier": "更早"
},
"empty": {
"title": "暂无事项",
"subtitle": "新的提及和分配会显示在这里。",
"selectTitle": "选择一个事项",
"selectSubtitle": "查看评论上下文或工作项详情。",
"mentions": {
"title": "暂无提及",
"subtitle": "当同事在评论中 @ 你时,会显示在这里。"
},
"assigned": {
"title": "暂无分配给你的事项",
"subtitle": "分配给你的工作项会显示在这里。"
},
"noResults": {
"title": "无匹配结果",
"subtitle": "没有与「{{query}}」匹配的事项。"
}
},
"loading": "正在加载团队收件箱…",
"loadMore": "加载更多",
"errors": {
"loadTitle": "无法加载团队收件箱",
"load": "无法加载团队收件箱",
"refresh": "无法刷新团队收件箱",
"markRead": "标记已读失败,请重试。",
"markUnread": "标记未读失败,请重试。",
"markAllRead": "全部标记已读失败,请重试。"
},
"detail": {
"assignedSubtitle": "分配给你的工作项",
"mentionSubtitle": "评论中提及了你",
"mentionedYou": "提及了你"
},
"actions": {
"markRead": "标记已读",
"markUnread": "标记未读",
"openWorkItem": "打开工作项",
"openSession": "打开会话"
},
"fields": {
"status": "状态",
"priority": "优先级",
"assignee": "负责人",
"workItemId": "工作项 ID",
"session": "会话",
"comments": "评论数",
"threadId": "话题 ID",
"commentId": "评论 ID"
},
"workItemStatus": {
"backlog": "待办池",
"todo": "待办",
"in_progress": "进行中",
"in_review": "审核中",
"blocked": "受阻",
"done": "已完成",
"cancelled": "已取消"
},
"priority": {
"none": "无优先级",
"low": "低",
"medium": "中",
"high": "高",
"urgent": "紧急"
}
},
"placeholderTypes": {
"mcpServer": "MCP 服务器",
"connection": "连接",
Expand Down
6 changes: 3 additions & 3 deletions src/modules/MainApp/TeamInbox/TeamInboxView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ const TeamInboxView: React.FC<TeamInboxViewProps> = ({
const [filter, setFilter] = useState<TeamInboxFilter>(initialFilter);
const [query, setQuery] = useState("");
const [items, setItems] = useState<TeamInboxItem[]>([]);
const [recencyAnchorMs, setRecencyAnchorMs] = useState(() => Date.now());
const [requestedItemId, setRequestedItemId] = useState<string | null>(null);
const [loadState, setLoadState] = useState<LoadState>({
status: "loading",
message: null,
});
const [reloadRevision, setReloadRevision] = useState(0);
const [groupingReferenceTime, setGroupingReferenceTime] = useState(0);
const [hasMore, setHasMore] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);

Expand All @@ -68,8 +68,8 @@ const TeamInboxView: React.FC<TeamInboxViewProps> = ({
.listPage({ limit: pageSize, signal: abortController.signal })
.then((page) => {
if (abortController.signal.aborted) return;
setGroupingReferenceTime(Date.now());
setItems(page.items);
setRecencyAnchorMs(Date.now());
setHasMore(page.nextCursor != null);
setLoadState({ status: "ready", message: null });
})
Expand Down Expand Up @@ -328,10 +328,10 @@ const TeamInboxView: React.FC<TeamInboxViewProps> = ({
<TeamInboxList
filter={filter}
items={visibleItems}
recencyAnchorMs={recencyAnchorMs}
selectedItemId={selectedItemId}
totalUnread={totalUnread}
unreadCounts={unreadCounts}
groupingReferenceTime={groupingReferenceTime}
query={query}
loading={loadState.status === "loading"}
onQueryChange={setQuery}
Expand Down
8 changes: 4 additions & 4 deletions src/modules/MainApp/TeamInbox/components/TeamInboxList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ import TeamInboxRow from "./TeamInboxRow";
export interface TeamInboxListProps {
filter: TeamInboxFilter;
items: readonly TeamInboxItem[];
recencyAnchorMs: number;
selectedItemId: string | null;
totalUnread: number;
unreadCounts: TeamInboxUnreadCounts;
groupingReferenceTime: number;
query: string;
loading: boolean;
onQueryChange: (query: string) => void;
Expand Down Expand Up @@ -61,10 +61,10 @@ function filterCountBadge(count: number, ariaLabel: string): React.ReactNode {
const TeamInboxList: React.FC<TeamInboxListProps> = ({
filter,
items,
recencyAnchorMs,
selectedItemId,
totalUnread,
unreadCounts,
groupingReferenceTime,
query,
loading,
onQueryChange,
Expand All @@ -85,8 +85,8 @@ const TeamInboxList: React.FC<TeamInboxListProps> = ({
[items, selectedItemId]
);
const groups = useMemo(
() => groupTeamInboxItemsByRecency(items, recencyAnchorMs),
[items, recencyAnchorMs]
() => groupTeamInboxItemsByRecency(items, groupingReferenceTime),
[groupingReferenceTime, items]
);
const activeFilterUnread = unreadCounts[filter];
const filterTabs = useMemo<TabPillItem[]>(
Expand Down
7 changes: 3 additions & 4 deletions src/modules/MainApp/TeamInbox/useTeamInboxDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,6 @@ export function useTeamInboxDataSource(): {
const viewerKey = `${viewerMemberIds.join("|")}::${authIdentityKey ?? "signed-out"}::${activeCloudOrgId ?? "local"}`;
const commentsSignals = useAtomValue(org2CloudCommentsSignalAtom);
const cloudReadReceipts = useAtomValue(teamInboxCloudReadReceiptsAtom);
const cloudReadReceiptsRef = useRef(cloudReadReceipts);
cloudReadReceiptsRef.current = cloudReadReceipts;
const setCloudReadReceipts = useSetAtom(teamInboxCloudReadReceiptsAtom);
const activeCloudCommentsRevision = activeCloudOrgId
? (commentsSignals[orgCommentsKey(activeCloudOrgId)] ?? 0)
Expand Down Expand Up @@ -284,7 +282,7 @@ export function useTeamInboxDataSource(): {
const cloudScopeKey = `${authIdentityKey ?? "signed-out"}|${activeCloudOrgId ?? "local"}`;
const overlaidMentions = overlayCloudReadReceipts(
mentionItems,
cloudReadReceiptsRef.current,
cloudReadReceipts,
cloudScopeKey
);
const mergedItems = [...overlaidMentions, ...localItems];
Expand Down Expand Up @@ -380,7 +378,7 @@ export function useTeamInboxDataSource(): {
const cloudScopeKey = `${authIdentityKey ?? "signed-out"}|${activeCloudOrgId ?? "local"}`;
const appendedMentions = overlayCloudReadReceipts(
mapMentionsToItems(cloudResult.mentions, activeCloudOrgId ?? ""),
cloudReadReceiptsRef.current,
cloudReadReceipts,
cloudScopeKey
);
const appended = resolveAssigneeDisplayNames(
Expand Down Expand Up @@ -512,6 +510,7 @@ export function useTeamInboxDataSource(): {
cache.error,
cache.hasMore,
cache.items,
cloudReadReceipts,
invalidate,
setCache,
setCloudReadReceipts,
Expand Down
Loading