Skip to content
Merged
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
1 change: 1 addition & 0 deletions apps/mobile/src/components/ComposerEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function ComposerEditor({
() => () => {
importRef.current?.abort();
},
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- Switching drafts aborts an import owned by the previous draft.
[draftKey],
);
const pasteContext = async (
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/components/MediaVideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ function LoadedMediaVideo(props: {
},
);
return () => controller.abort();
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- A play request or Retry (attempt) reloads the source into the same player.
}, [player, props.playRequested, attempt]);

return (
Expand Down
6 changes: 4 additions & 2 deletions apps/mobile/src/components/VideoPreviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ function useLocalPlayback(source: LocalVideoPreviewSource): PlaybackState {
() => undefined,
);
};
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- A different attachment restarts the load even when it resolves to the same cached file URI.
}, [attachmentId, fileUri]);
const mimeType = videoMimeType(attachment) ?? attachment.mimeType;
return {
Expand Down Expand Up @@ -208,9 +209,10 @@ export function VideoPreviewModal(props: {
}) {
const isFocused = useIsFocused();
const hasSource = props.source !== null;
const { onRequestClose } = props;
useEffect(() => {
if (!isFocused && hasSource) props.onRequestClose();
}, [isFocused, hasSource, props.onRequestClose]);
if (!isFocused && hasSource) onRequestClose();
}, [isFocused, hasSource, onRequestClose]);
const { source } = props;
if (source === null || !isFocused) return null;
return source.type === "local" ? (
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ function useNativeReviewDiffPayload(
cancelAnimationFrame(frame);
}
};
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- A new resetKey redelivers an unchanged payload after the diff surface resets.
}, [method, nativeRef, payload, resetKey, onFailure, onDelivered]);
}

Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/files/SourceFileSurface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) {
listRef.current?.scrollToIndex({ index: targetIndex, animated: false, viewPosition: 0.3 });
});
return () => cancelAnimationFrame(frame);
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- Opening another file at the same line index scrolls the new file to it.
}, [props.path, targetIndex]);

const renderLine = ({ item, index }: { item: string; index: number }) => (
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ export function HomeScreen(props: HomeScreenProps) {
bumpSnoozeWakeTick((tick) => tick + 1);
}, delayMs);
return () => clearTimeout(id);
// snoozeWakeTick re-arms clamped timers even when the wake date exceeds 32-bit setTimeout and stays unchanged.
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- snoozeWakeTick re-arms clamped timers even when the wake date exceeds 32-bit setTimeout and stays unchanged.
}, [nextSnoozeWakeAt, snoozeWakeTick]);
// Called as `props.onSelectThread(...)`, the compiler would cache this handler on the whole
// props object, which is new on every render of Home's parent.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export function useRegisterWorkspaceInspector(render: (() => ReactNode) | undefi
if (focusedRef.current) {
syncRegistration();
}
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- Changed pane content replaces its existing native workspace registration.
}, [wrappedRender]);

useEffect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ export function PullRequestRouteScreen({ route }: StaticScreenProps<PullRequestP
currentScrollOffset.current = scrollOffsets.current.overview ?? -insets.top;
setSection("overview");
}
}, [guidesEnabled, section, insets.top]);
}, [guidesEnabled, section, insets.top, currentScrollOffset, requestedSection, scrollOffsets]);
const tabContent = activityContent;
const filesY = useRef(0);
const descriptionY = useRef(0);
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/review/ReviewSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ export function ReviewSheet(props: ReviewSheetProps) {

useEffect(() => {
showAuxiliaryPane("inspector");
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- Opening review for another thread reselects its inspector pane.
}, [environmentId, showAuxiliaryPane, threadId]);
const {
error,
Expand Down
7 changes: 5 additions & 2 deletions apps/mobile/src/features/threads/NewTaskDraftScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ export function NewTaskDraftScreen(props: {
projects,
flow.draftKey,
props.initialProjectRef,
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- Opening a draft from a share, pending task, or draft id re-resolves its project.
props.incomingShareId,
props.pendingTaskId,
props.draftId,
Expand All @@ -522,6 +523,7 @@ export function NewTaskDraftScreen(props: {
setProject,
]);

const { loadBranches } = flow;
useEffect(() => {
if (!selectedProject) {
loadedBranchesProjectKeyRef.current = null;
Expand All @@ -532,8 +534,8 @@ export function NewTaskDraftScreen(props: {
return;
}
loadedBranchesProjectKeyRef.current = projectKey;
flow.loadBranches();
}, [flow.loadBranches, selectedProject]);
loadBranches();
}, [loadBranches, selectedProject]);

useEffect(() => {
const shareId = props.incomingShareId;
Expand Down Expand Up @@ -745,6 +747,7 @@ export function NewTaskDraftScreen(props: {
consumeShare,
cancelledIncomingShareId,
flow.draftKey,
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- hasImportedIncomingShare reflects the draft store; a change re-checks whether the share still needs importing.
hasImportedIncomingShare,
incomingShare,
isIncomingShareInboxLoading,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export function ThreadArrangementSheet(props: { onClose: () => void }) {
.map((row) => `${row.key}:${row.thread?.pinOrderKey}:${row.thread?.activeOrderKey}`)
.join("|");
useEffect(() => {
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- A changed thread order cancels the in-progress drag; stop only touches refs and state setters.
stop();
}, [orderVersion]);
useEffect(
Expand Down
5 changes: 3 additions & 2 deletions apps/mobile/src/features/threads/ThreadComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -764,11 +764,12 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps
settingsSheetPresentation.open();
};

const { present: presentSettingsRoute } = settingsRoutePresentation;
useEffect(() => {
if (settingsSheetPresentation.isActive) {
settingsRoutePresentation.present(settingsRouteSession);
presentSettingsRoute(settingsRouteSession);
}
}, [settingsRoutePresentation.present, settingsRouteSession, settingsSheetPresentation.isActive]);
}, [presentSettingsRoute, settingsRouteSession, settingsSheetPresentation.isActive]);

useEffect(() => {
if (!settingsSheetPresentation.isVisible || settingsRoutePresentedRef.current) {
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/threads/ThreadDetailScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ function ThreadDetailBody(props: ThreadDetailScreenProps) {
}, []);
useEffect(() => {
setKeyboardStateSuspect(false);
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- Any keyboard visibility or height report clears the suspect flag set when the app resumed.
}, [isKeyboardVisible, liveKeyboardHeight]);
const handleOwnedInputFocusChange = (focused: boolean) => {
ownedInputFocusedRef.current = focused;
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2197,6 +2197,7 @@ export const ThreadFeed = memo(function ThreadFeed({
props.threadId,
props.contentPresentation.kind,
]);
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- A scope change (thread or presentation) aborts the pending link lookup opened from the old scope.
useLayoutEffect(() => () => markdownLinkRequest.current?.abort(), [markdownLinkScope]);
const [preview, setPreview] = useState<{
readonly scope: string;
Expand Down Expand Up @@ -2772,6 +2773,7 @@ export const ThreadFeed = memo(function ThreadFeed({
// commit, and setEndFollow notifies the parent, so it can't run during render.
useEffect(() => {
if (jumpIndex !== null) landJump(jumpIndex);
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- Jumping to a different message whose row index is unchanged must still land the jump.
}, [jumpToMessageId, jumpIndex]);

const reflowLayout = () => ({
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/features/threads/ThreadMarkdownImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ export function ThreadMarkdownImageView(props: {

useEffect(() => {
setDecodedSize(null);
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- A new source invalidates the decoded image dimensions.
}, [props.sourceKey]);

useEffect(() => {
setFailedUri(null);
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- A new image URI clears the previous load failure.
}, [props.uri]);

const sourceSize = decodedSize ?? props.knownSize ?? null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ function ThreadNavigationSidebarPane(
bumpSnoozeWakeTick((tick) => tick + 1);
}, delayMs);
return () => clearTimeout(id);
// snoozeWakeTick re-arms clamped timers even when the wake date exceeds 32-bit setTimeout and stays unchanged.
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- snoozeWakeTick re-arms clamped timers even when the wake date exceeds 32-bit setTimeout and stays unchanged.
}, [nextSnoozeWakeAt, snoozeWakeTick]);
const listItems: readonly SidebarListItem[] =
settledShelfExpanded && threadListV2Layout.hiddenSettledCount > 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ export function useComposerCommandMenu({
else failedRefreshesRef.current = null;
}, retryLater);
}, [
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- Typing in the draft re-checks whether the provider workspace snapshot needs refreshing.
draftMessage,
environmentId,
projectCwd,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export function WorkspaceToolbar(props: {
if (!tabActive) return;
const frame = requestAnimationFrame(restoreSearch);
return () => cancelAnimationFrame(frame);
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- Moving the search bar to the bottom toolbar restores its focus and query.
}, [restoreSearch, tabActive, bottomSearch]);
const composeButton = nativeToolbarCompose ? (
<Toolbar.Button
Expand Down
11 changes: 9 additions & 2 deletions apps/mobile/src/native/T3ComposerEditor.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ export function ComposerEditor({
nativeEventSnapshotsRef.current,
mostRecentEventCount,
);
}, [acknowledgesLatestNativeEvent, mostRecentEventCount]);
}, [acknowledgesLatestNativeEvent, mostRecentEventCount, nativeEventSnapshotsRef]);
const assumedValue = props.value;
useEffect(() => {
// Newer native events make this payload stale; do not record it as applied.
Expand All @@ -246,7 +246,14 @@ export function ComposerEditor({
controlledEventCount,
assumedValue,
);
}, [assumedValue, controlledEventCount, isNativeEcho, controlledDocumentJson]);
}, [
assumedValue,
controlledEventCount,
isNativeEcho,
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- Every changed native payload, including selection-only changes, must update the assumed native snapshot.
controlledDocumentJson,
nativeEventSnapshotsRef,
]);
const acceptNativeEvent = useCallback(
(eventCount: number, value: string, nextSelection: ComposerEditorSelection) => {
const acknowledgedEventCount = acknowledgeComposerNativeEvent(
Expand Down
11 changes: 9 additions & 2 deletions apps/mobile/src/native/T3ComposerEditor.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ export function ComposerEditor({
nativeEventSnapshotsRef.current,
mostRecentEventCount,
);
}, [acknowledgesLatestNativeEvent, mostRecentEventCount]);
}, [acknowledgesLatestNativeEvent, mostRecentEventCount, nativeEventSnapshotsRef]);
const assumedValue = props.value;
useEffect(() => {
// Newer native events make this payload stale; do not record it as applied.
Expand All @@ -243,7 +243,14 @@ export function ComposerEditor({
controlledEventCount,
assumedValue,
);
}, [assumedValue, controlledEventCount, isNativeEcho, controlledDocumentJson]);
}, [
assumedValue,
controlledEventCount,
isNativeEcho,
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- Every changed native payload, including selection-only changes, must update the assumed native snapshot.
controlledDocumentJson,
nativeEventSnapshotsRef,
]);
const acceptNativeEvent = useCallback(
(eventCount: number, value: string, nextSelection: ComposerEditorSelection) => {
const acknowledgedEventCount = acknowledgeComposerNativeEvent(
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/state/thread-order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function usePendingThreadOrder(nowMinute: string, snoozeWakeTick: number)
// Clock-driven wakes can change membership without shell events; retire order holds and re-enable moves then.
useEffect(() => {
getPendingThreadOrder();
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- Clock ticks re-evaluate the external pending-order snapshot even without a shell event.
}, [nowMinute, snoozeWakeTick]);
return pending;
}
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/state/use-thread-outbox-drain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1359,6 +1359,7 @@ export function useThreadOutboxDrain(): void {
operableEnvironments,
projects,
queuedMessagesByThreadKey,
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- retryTick re-runs the drain when a retry timer fires.
retryTick,
restoreQueuedMessage,
scheduleQueuedMessageRetry,
Expand Down
1 change: 1 addition & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export default defineConfig({
"react/memo-dependencies": "error",
"react/purity": "error",
"react/refs": "error",
"react/exhaustive-effect-dependencies": "error",
"react/immutability": "error",
"react/no-array-index-key": "error",
"react/no-unstable-nested-components": "error",
Expand Down
Loading