Repository navigation
feat: animate chat input icons on focus #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
eca264d
feat: show notification permission rationale before first model download
github-actions[bot] 4e45067
feat: animate chat input icons on focus to maximize typing area
github-actions[bot] 7ce3bbd
fix: remove duplicate notification permission rationale dialog
github-actions[bot] 39278a3
refactor: extract pill icon magic number into named constants
github-actions[bot] 3b9aa54
refactor: extract useNotifRationale hook and fix lint errors
github-actions[bot] 308cc7a
refactor: extract animation durations into named constants
github-actions[bot] c9446c8
fix: add translateX to icon focus animation for visible slide-out
github-actions[bot] fbed3e4
fix: simplify icon animation to width collapse with fast opacity fade
github-actions[bot] d67f4d3
test: add focus animation and notification rationale tests
github-actions[bot] 299e6e0
fix: show notification rationale only once per session + add overflow…
github-actions[bot] 937254f
refactor: trigger icon collapse on text content instead of focus/blur
github-actions[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,116 @@ | ||
| import { renderHook, act } from '@testing-library/react-native'; | ||
| import { Platform, PermissionsAndroid } from 'react-native'; | ||
| import { useNotifRationale } from '../../../src/screens/ModelsScreen/useNotifRationale'; | ||
|
|
||
| const mockRequestNotificationPermission = jest.fn().mockResolvedValue(undefined); | ||
| jest.mock('../../../src/services', () => ({ | ||
| backgroundDownloadService: { | ||
| get requestNotificationPermission() { return mockRequestNotificationPermission; }, | ||
| }, | ||
| })); | ||
|
|
||
| jest.mock('../../../src/utils/logger', () => ({ | ||
| __esModule: true, | ||
| default: { warn: jest.fn() }, | ||
| })); | ||
|
|
||
| function setupAndroid33(permissionGranted: boolean) { | ||
| Object.defineProperty(Platform, 'OS', { get: () => 'android' }); | ||
| Object.defineProperty(Platform, 'Version', { get: () => 33 }); | ||
| jest.spyOn(PermissionsAndroid, 'check').mockResolvedValue(permissionGranted); | ||
| } | ||
|
|
||
| async function renderAndTrigger(isFirstDownload: boolean) { | ||
| const proceed = jest.fn(); | ||
| const hook = renderHook(() => useNotifRationale(isFirstDownload)); | ||
| await act(async () => { | ||
| await hook.result.current.maybeShowNotifRationale(proceed); | ||
| }); | ||
| return { ...hook, proceed }; | ||
| } | ||
|
|
||
| describe('useNotifRationale', () => { | ||
| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| it('proceeds immediately when not first download', async () => { | ||
| const { result, proceed } = await renderAndTrigger(false); | ||
|
|
||
| expect(proceed).toHaveBeenCalled(); | ||
| expect(result.current.showNotifRationale).toBe(false); | ||
| }); | ||
|
|
||
| it('proceeds immediately on iOS', async () => { | ||
| Object.defineProperty(Platform, 'OS', { get: () => 'ios' }); | ||
| const { result, proceed } = await renderAndTrigger(true); | ||
|
|
||
| expect(proceed).toHaveBeenCalled(); | ||
| expect(result.current.showNotifRationale).toBe(false); | ||
| Object.defineProperty(Platform, 'OS', { get: () => 'android' }); | ||
| }); | ||
|
|
||
| it('proceeds immediately on Android < 33', async () => { | ||
| Object.defineProperty(Platform, 'OS', { get: () => 'android' }); | ||
| Object.defineProperty(Platform, 'Version', { get: () => 32 }); | ||
| const { result, proceed } = await renderAndTrigger(true); | ||
|
|
||
| expect(proceed).toHaveBeenCalled(); | ||
| expect(result.current.showNotifRationale).toBe(false); | ||
| }); | ||
|
|
||
| it('proceeds immediately when permission already granted', async () => { | ||
| setupAndroid33(true); | ||
| const { result, proceed } = await renderAndTrigger(true); | ||
|
|
||
| expect(proceed).toHaveBeenCalled(); | ||
| expect(result.current.showNotifRationale).toBe(false); | ||
| }); | ||
|
|
||
| it('shows rationale on Android 33+ first download without permission', async () => { | ||
| setupAndroid33(false); | ||
| const { result, proceed } = await renderAndTrigger(true); | ||
|
|
||
| expect(proceed).not.toHaveBeenCalled(); | ||
| expect(result.current.showNotifRationale).toBe(true); | ||
| }); | ||
|
|
||
| it('handleNotifRationaleAllow requests permission then proceeds', async () => { | ||
| setupAndroid33(false); | ||
| const { result, proceed } = await renderAndTrigger(true); | ||
| expect(result.current.showNotifRationale).toBe(true); | ||
|
|
||
| await act(async () => { result.current.handleNotifRationaleAllow(); }); | ||
| await act(async () => { await Promise.resolve(); }); | ||
|
|
||
| expect(mockRequestNotificationPermission).toHaveBeenCalled(); | ||
| expect(proceed).toHaveBeenCalled(); | ||
| expect(result.current.showNotifRationale).toBe(false); | ||
| }); | ||
|
|
||
| it('only shows rationale once per session', async () => { | ||
| setupAndroid33(false); | ||
| const { result, proceed } = await renderAndTrigger(true); | ||
| expect(proceed).not.toHaveBeenCalled(); | ||
|
|
||
| await act(async () => { result.current.handleNotifRationaleDismiss(); }); | ||
|
|
||
| const proceed2 = jest.fn(); | ||
| await act(async () => { | ||
| await result.current.maybeShowNotifRationale(proceed2); | ||
| }); | ||
| expect(proceed2).toHaveBeenCalled(); | ||
| expect(result.current.showNotifRationale).toBe(false); | ||
| }); | ||
|
|
||
| it('handleNotifRationaleDismiss proceeds without requesting permission', async () => { | ||
| setupAndroid33(false); | ||
| const { result, proceed } = await renderAndTrigger(true); | ||
|
|
||
| await act(async () => { result.current.handleNotifRationaleDismiss(); }); | ||
|
|
||
| expect(mockRequestNotificationPermission).not.toHaveBeenCalled(); | ||
| expect(proceed).toHaveBeenCalled(); | ||
| expect(result.current.showNotifRationale).toBe(false); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While
overflow: 'hidden'is applied to the parentpillcontainer, it is safer to also apply it directly to thisAnimated.View. This ensures that as the width collapses to 0, the icon buttons (which have a fixed width of 36) are clipped immediately and don't potentially overlap with theTextInputor other elements if the parent's clipping doesn't align perfectly with the shrinking container.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added
overflow: 'hidden'directly on the iconsAnimated.Viewfor safer clipping. Fixed in 299e6e0.