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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Do not add new exported functions, types, or constants unless they are required outside the file. Prefer file-local helpers for one-off implementation details and tests.
- Under `shared/`, non-test TypeScript source files should use the `.tsx` extension.
- Do not edit lockfiles by hand. They are generated artifacts. If you cannot regenerate one locally, leave it unchanged.
- Never disable lints to address lint failures. Fix the underlying issue instead.
- Components must not mutate Zustand stores directly with `useXState.setState`, `getState()`-based writes, or similar ad hoc store mutation. If a component needs to affect store state, route it through a store dispatch action or move the state out of the store.
- For server-owned state such as badges, Gregor-driven UI state, and other engine-fed state, prefer reflecting the latest server state instead of masking problems with optimistic local mutations. Do not add local state writes that make the UI look correct while drifting from what the server has actually told us.
- When a Zustand store already uses `resetState: Z.defaultReset`, prefer calling `dispatch.resetState()` for full resets instead of manually reassigning each initial field in another dispatch action.
Expand Down
11 changes: 8 additions & 3 deletions shared/app/global-errors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,22 @@ const useData = () => {

const [cachedSummary, setSummary] = React.useState(summaryForError(error))
const [cachedDetails, setDetails] = React.useState(detailsForError(error))
const [size, setSize] = React.useState<Size>('Closed')
const [expandedError, setExpandedError] = React.useState<Error | RPCError>()
const countdownTimerRef = React.useRef<undefined | ReturnType<typeof setTimeout>>(undefined)
if (!error && expandedError) {
setExpandedError(undefined)
}
const size: Size = error ? (expandedError === error ? 'Big' : 'Small') : 'Closed'

const clearCountdown = () => {
countdownTimerRef.current && clearTimeout(countdownTimerRef.current)
countdownTimerRef.current = undefined
}

const onExpandClick = () => {
setSize('Big')
if (error) {
setExpandedError(error)
}
if (!C.isMobile) {
clearCountdown()
}
Expand All @@ -73,7 +79,6 @@ const useData = () => {
error ? 0 : 7000
) // if it's set, do it immediately, if it's cleared set it in a bit
const newError = !!error
setSize(newError ? 'Small' : 'Closed')
if (!C.isMobile) {
if (countdownTimerRef.current) clearTimeout(countdownTimerRef.current)
countdownTimerRef.current = undefined
Expand Down
42 changes: 20 additions & 22 deletions shared/app/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,32 +32,29 @@ module.hot?.accept(() => {
console.log('accepted update in shared/index.native')
})

const initDarkMode = () => {
const {setDarkModePreference, setSystemDarkMode, setSystemSupported} =
DarkMode.useDarkModeState.getState().dispatch
setSystemDarkMode(Appearance.getColorScheme() === 'dark')
setSystemSupported(darkModeSupported)
try {
const obj = JSON.parse(guiConfig) as {ui?: {darkMode?: string}} | undefined
const dm = obj?.ui?.darkMode
switch (dm) {
case 'system': // fallthrough
case 'alwaysDark': // fallthrough
case 'alwaysLight':
setDarkModePreference(dm, false)
break
default:
}
} catch {}
}

const useDarkHookup = () => {
const initedRef = React.useRef(false)
const appStateRef = React.useRef('active')
const setSystemDarkMode = DarkMode.useDarkModeState(s => s.dispatch.setSystemDarkMode)
const setMobileAppState = useShellState(s => s.dispatch.setMobileAppState)
const setSystemSupported = DarkMode.useDarkModeState(s => s.dispatch.setSystemSupported)
const setDarkModePreference = DarkMode.useDarkModeState(s => s.dispatch.setDarkModePreference)

// once
if (!initedRef.current) {
initedRef.current = true
setSystemDarkMode(Appearance.getColorScheme() === 'dark')
setSystemSupported(darkModeSupported)
try {
const obj = JSON.parse(guiConfig) as {ui?: {darkMode?: string}} | undefined
const dm = obj?.ui?.darkMode
switch (dm) {
case 'system': // fallthrough
case 'alwaysDark': // fallthrough
case 'alwaysLight':
setDarkModePreference(dm, false)
break
default:
}
} catch {}
}

React.useEffect(() => {
const appStateChangeSub = AppState.addEventListener('change', nextAppState => {
Expand Down Expand Up @@ -124,6 +121,7 @@ let inited = false
const useInit = () => {
if (inited) return
inited = true
initDarkMode()
Animated.addWhitelistedNativeProps({text: true})
install()
const {batch} = C.useWaitingState.getState().dispatch
Expand Down
18 changes: 11 additions & 7 deletions shared/chat/audio/audio-recorder.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,9 @@ const useRecorder = (p: {ampSV: SVN; setShowAudioSend: (s: boolean) => void; sho
const recordEndRef = React.useRef(0)
const hasSetupRecording = React.useRef(false)
const pathRef = React.useRef('')
const ampTracker = React.useRef(new AmpTracker()).current
const [ampTracker] = React.useState(() => new AmpTracker())
const [staged, setStaged] = React.useState(false)
const [stagedRecording, setStagedRecording] = React.useState({duration: 0, path: ''})

const recorder = useAudioRecorder(recordingOptions)
const recorderState = useAudioRecorderState(recorder, 100)
Expand Down Expand Up @@ -361,6 +362,7 @@ const useRecorder = (p: {ampSV: SVN; setShowAudioSend: (s: boolean) => void; sho
}
recordStartRef.current = 0
recordEndRef.current = 0
setStagedRecording({duration: 0, path: ''})
setStaged(false)
setShowAudioSend(false)
}
Expand Down Expand Up @@ -451,15 +453,19 @@ const useRecorder = (p: {ampSV: SVN; setShowAudioSend: (s: boolean) => void; sho
<AudioSend
ampTracker={ampTracker}
cancelRecording={cancelRecording}
duration={(recordEndRef.current || recordStartRef.current) - recordStartRef.current}
path={pathRef.current}
duration={stagedRecording.duration}
path={stagedRecording.path}
sendRecording={sendRecording}
/>
) : null

const stageRecording = () => {
const impl = async () => {
await stopRecording()
setStagedRecording({
duration: (recordEndRef.current || recordStartRef.current) - recordStartRef.current,
path: pathRef.current,
})
setStaged(true)
setShowAudioSend(true)
}
Expand All @@ -469,13 +475,11 @@ const useRecorder = (p: {ampSV: SVN; setShowAudioSend: (s: boolean) => void; sho
}

// on unmount cleanup
const onResetRef = React.useRef(onReset)
onResetRef.current = onReset
const onResetEvent = React.useEffectEvent(onReset)
React.useEffect(() => {
return () => {
setShowAudioSend(false)
onResetRef
.current()
onResetEvent()
.then(() => {})
.catch(() => {})
}
Expand Down
52 changes: 18 additions & 34 deletions shared/chat/blocking/block-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,11 +209,26 @@ const Container = function BlockModal(ownProps: OwnProps) {
navigateUp()
}
}
const [blockTeam, setBlockTeam] = React.useState(true)
const [blockTeam, setBlockTeam] = React.useState(context !== 'message-popup')
const [finishClicked, setFinishClicked] = React.useState(false)
// newBlocks holds a Map of blocks that will be applied when user clicks
// "Finish" button. reports is the same thing for reporting.
const [newBlocks, setNewBlocks] = React.useState(new Map())
const [newBlocks, setNewBlocks] = React.useState<NewBlocksMap>(() => {
const initialBlocks = new Map<string, BlocksForUser>()
if (blockUserByDefault && adderUsername) {
initialBlocks.set(adderUsername, {
chatBlocked: true,
followBlocked: true,
report: reportsUserByDefault
? {
...defaultReport,
...(flagUserByDefault ? {reason: reasons[reasons.length - 2] ?? defaultReport.reason} : {}),
}
: undefined,
})
}
return initialBlocks
})

const loadedOnceRef = React.useRef(false)
React.useEffect(() => {
Expand All @@ -226,38 +241,7 @@ const Container = function BlockModal(ownProps: OwnProps) {
if (usernames.length) {
refreshBlocksFor(usernames)
}

// Set default checkbox block values for adder user. We don't care if they
// are already blocked, setting a block is idempotent.
if (blockUserByDefault && adderUsername) {
const map = newBlocks
map.set(adderUsername, {
chatBlocked: true,
followBlocked: true,
report: reportsUserByDefault
? {
...defaultReport,
...(flagUserByDefault ? {reason: reasons[reasons.length - 2]} : {}),
}
: undefined,
})
setNewBlocks(new Map(map))
}
if (context === 'message-popup') {
// Do not block conversation by default when coming from message popup
// menu.
setBlockTeam(false)
}
}, [
adderUsername,
blockUserByDefault,
context,
flagUserByDefault,
newBlocks,
otherUsernames,
refreshBlocksFor,
reportsUserByDefault,
])
}, [adderUsername, otherUsernames, refreshBlocksFor])

const lastFinishWaitingRef = React.useRef(finishWaiting)
React.useEffect(() => {
Expand Down
8 changes: 5 additions & 3 deletions shared/chat/conversation/attachment-get-titles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,11 @@ const Container = (ownProps: OwnProps) => {
const inputRef = React.useRef<Kb.Input3Ref>(null)

const {info, path} = pathAndInfos[index] ?? {}
const [kbfsPreviewURL, setKbfsPreviewURL] = React.useState<string | undefined>(undefined)
const [kbfsPreview, setKbfsPreview] = React.useState<
{path: string; url: string | undefined} | undefined
>()
const kbfsPreviewURL = kbfsPreview && kbfsPreview.path === path ? kbfsPreview.url : undefined
React.useEffect(() => {
setKbfsPreviewURL(undefined)
if (info?.type !== 'image' || info.url || !path || !isKbfsPath(path)) {
return
}
Expand All @@ -130,7 +132,7 @@ const Container = (ownProps: OwnProps) => {
path: FS.pathToRPCPath(T.FS.stringToPath(path)).kbfs,
})
if (!canceled) {
setKbfsPreviewURL(fileContext.url)
setKbfsPreview({path, url: fileContext.url})
}
} catch {}
}
Expand Down
38 changes: 25 additions & 13 deletions shared/chat/conversation/bot/install.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,20 @@ export const useRefreshBotMembershipOnSuccess = (

export const useBotConversationIDKey = (inConvIDKey?: T.Chat.ConversationIDKey, teamID?: T.Teams.TeamID) => {
const cleanInConvIDKey = T.Chat.isValidConversationIDKey(inConvIDKey ?? '') ? inConvIDKey : undefined
const [conversationIDKey, setConversationIDKey] = React.useState(cleanInConvIDKey)
const [generalConversation, setGeneralConversation] = React.useState<
| {
conversationIDKey: T.Chat.ConversationIDKey
teamID: T.Teams.TeamID
}
| undefined
>()
const findGeneralConvIDFromTeamID = C.useRPC(T.RPCChat.localFindGeneralConvFromTeamIDRpcPromise)
const requestIDRef = React.useRef(0)

React.useEffect(() => {
setConversationIDKey(cleanInConvIDKey)
}, [cleanInConvIDKey])
const conversationIDKey =
cleanInConvIDKey ??
(generalConversation && generalConversation.teamID === teamID
? generalConversation.conversationIDKey
: undefined)

React.useEffect(() => {
requestIDRef.current += 1
Expand All @@ -85,7 +92,7 @@ export const useBotConversationIDKey = (inConvIDKey?: T.Chat.ConversationIDKey,
return
}
ConvoState.metasReceived([meta])
setConversationIDKey(meta.conversationIDKey)
setGeneralConversation({conversationIDKey: meta.conversationIDKey, teamID})
},
() => {}
)
Expand Down Expand Up @@ -135,7 +142,13 @@ const InstallBotPopup = (props: Props) => {
const [installWithRestrict, setInstallWithRestrict] = React.useState(true)
const [installInConvs, setInstallInConvs] = React.useState<ReadonlyArray<string>>([])
const [disableDone, setDisableDone] = React.useState(false)
const [botPublicCommands, setBotPublicCommands] = React.useState<T.Chat.BotPublicCommands | undefined>()
const [loadedBotPublicCommands, setLoadedBotPublicCommands] = React.useState<
| {
botUsername: string
commands: T.Chat.BotPublicCommands
}
| undefined
>()

const meta = ConvoState.useChatContext(s => s.meta)
const commandsFromMeta = (
Expand All @@ -148,7 +161,9 @@ const InstallBotPopup = (props: Props) => {
const commands =
commandsFromMeta.length > 0
? ({commands: commandsFromMeta, loadError: false} satisfies T.Chat.BotPublicCommands)
: botPublicCommands
: loadedBotPublicCommands?.botUsername === botUsername
? loadedBotPublicCommands.commands
: undefined

const featured = useFeaturedBot(botUsername)
const teamRole = ConvoState.useChatContext(s => s.botTeamRoleMap.get(botUsername))
Expand Down Expand Up @@ -238,9 +253,6 @@ const InstallBotPopup = (props: Props) => {
const loadBotPublicCommands = C.useRPC(T.RPCChat.localListPublicBotCommandsLocalRpcPromise)
const botPublicCommandsRequestIDRef = React.useRef(0)
const clearedWaitingForBotRef = React.useRef<string | undefined>(undefined)
React.useEffect(() => {
setBotPublicCommands(undefined)
}, [botUsername])
React.useEffect(() => {
if (!mutationWaiting && clearedWaitingForBotRef.current !== botUsername) {
clearedWaitingForBotRef.current = botUsername
Expand All @@ -260,13 +272,13 @@ const InstallBotPopup = (props: Props) => {
return
}
const commands = (res.commands ?? []).map(command => command.name)
setBotPublicCommands({commands, loadError: false})
setLoadedBotPublicCommands({botUsername, commands: {commands, loadError: false}})
},
() => {
if (botPublicCommandsRequestIDRef.current !== requestID) {
return
}
setBotPublicCommands({commands: [], loadError: true})
setLoadedBotPublicCommands({botUsername, commands: {commands: [], loadError: true}})
}
)
return () => {
Expand Down
20 changes: 8 additions & 12 deletions shared/chat/conversation/info-panel/attachments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,6 @@ export const useAttachmentSections = (
useFlexWrap: boolean
): {sections: Array<Section>} => {
const [selectedAttachmentView, onSelectAttachmentView] = React.useState(T.RPCChat.GalleryItemTyp.media)
const [lastSAV, setLastSAV] = React.useState(selectedAttachmentView)
const loadAttachmentView = ConvoState.useChatContext(s => s.dispatch.loadAttachmentView)
const loadMessagesCentered = ConvoState.useChatContext(s => s.dispatch.loadMessagesCentered)
const clearModals = C.Router2.clearModals
Expand All @@ -448,25 +447,22 @@ export const useAttachmentSections = (
}, 1)
})

React.useEffect(() => {
if (lastSAV !== selectedAttachmentView) {
setLastSAV(selectedAttachmentView)
if (loadImmediately) {
setTimeout(() => {
loadAttachmentView(selectedAttachmentView)
}, 1)
}
}
}, [lastSAV, loadAttachmentView, loadImmediately, selectedAttachmentView])

const attachmentView = ConvoState.useChatContext(s => s.attachmentViewMap)
const attachmentInfo = attachmentView.get(selectedAttachmentView)
const fromMsgID = attachmentInfo ? getFromMsgID(attachmentInfo) : undefined

const onLoadMore = fromMsgID ? () => loadAttachmentView(selectedAttachmentView, fromMsgID) : undefined

const onAttachmentViewChange = (viewType: T.RPCChat.GalleryItemTyp) => {
if (viewType === selectedAttachmentView) {
return
}
onSelectAttachmentView(viewType)
if (loadImmediately) {
setTimeout(() => {
loadAttachmentView(viewType)
}, 1)
}
}

const loadAttachments = () => {
Expand Down
Loading