Skip to content

feat: animate chat input icons on focus - #72

Merged
alichherawalla merged 11 commits into
mainfrom
feat/chat-input-icon-animation
Feb 23, 2026
Merged

alichherawalla merged 11 commits into
mainfrom
feat/chat-input-icon-animation

Conversation

@alichherawalla

Copy link
Copy Markdown
Collaborator

Summary

  • When the chat input gains focus, the three right-side pill icons (paperclip, eye, image) collapse their width to 0 and fade out, giving the TextInput the full pill width for typing
  • On blur, icons smoothly restore
  • The send/stop/mic circular button remains always visible so users can send while typing

Type of Change

  • New feature (non-breaking change that adds functionality)

Screenshots / Screen Recordings

Android

Before After
Icons always visible, input area constrained Icons collapse on focus, full-width typing area

iOS

Before After
Icons always visible, input area constrained Icons collapse on focus, full-width typing area

Checklist

General

  • My code follows the project's coding style and conventions
  • I have performed a self-review of my code
  • I have added/updated comments where the logic isn't self-evident
  • My changes generate no new warnings or errors

Testing

  • I have tested on Android (physical device or emulator)
  • I have tested on iOS (physical device or simulator)
  • I have tested in light mode and dark mode
  • Existing tests pass locally (npm test)
  • I have added tests that prove my fix is effective or my feature works

React Native Specific

  • No new native module without corresponding platform implementation (Android + iOS)
  • New native modules are added to the Xcode project build target (project.pbxproj)
  • No hardcoded pixel values — uses SPACING / TYPOGRAPHY constants from the theme
  • Styles use useThemedStyles pattern (not inline or static StyleSheet.create)
  • Animations/gestures work smoothly on both platforms
  • Large lists use FlatList / FlashList (not .map() inside ScrollView)
  • No unnecessary re-renders introduced (check with React DevTools Profiler if unsure)

Security

  • No secrets, API keys, or credentials are included in the code
  • User input is validated/sanitized where applicable

Additional Notes

  • Uses a single Animated.Value with useNativeDriver: false to animate both width (108→0) and opacity (1→0) on the icons container. Width animation is required to actually reclaim flex layout space (translateX alone only moves visually).
  • pointerEvents='none' prevents hidden icons from intercepting touches during/after fade-out.
  • overflow: 'hidden' on the pill clips icon content cleanly as the container width shrinks.
  • useCallback wraps focus/blur handlers to avoid unnecessary re-renders.

github-actions Bot and others added 2 commits February 23, 2026 17:10
On Android 13+, intercept the first download attempt on the Models
screen to explain why notification permission is needed and that it is
optional. The OS dialog only appears if the user taps Allow; either
way the download proceeds unblocked.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the chat input gains focus, the three right-side icons (paperclip,
eye, image) collapse and fade out, giving the TextInput the full pill
width for typing. On blur they smoothly restore. The send/stop/mic
button remains always visible so users can send while typing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @alichherawalla, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the chat input user experience by introducing a dynamic animation for the accessory icons, allowing for a more expansive typing area when the input is active. Concurrently, it refines the application's approach to requesting notification permissions on Android, ensuring that users are prompted contextually and only when necessary, specifically before their initial download.

Highlights

  • Chat Input Animation: The chat input icons (paperclip, eye, image) now animate their width and opacity, collapsing and fading out when the text input gains focus, and smoothly restoring on blur. This provides a full-width typing area.
  • Notification Permission Rationale: A new user-facing alert has been implemented to conditionally request POST_NOTIFICATIONS permission on Android 13+ only for the first download, improving user experience and compliance.
  • Technical Implementation: The animation uses React Native's Animated API with useNativeDriver: false for width and opacity interpolation. pointerEvents='none' is used to prevent interaction with hidden icons, and overflow: 'hidden' on the pill clips content during collapse.
Changelog
  • App.tsx
    • Removed immediate notification permission request on app startup.
  • src/components/ChatInput/index.tsx
    • Introduced Animated API and useCallback hook for animation logic.
    • Implemented isFocused state and focusAnim Animated.Value to control icon animations.
    • Added handleFocus and handleBlur callbacks to trigger animations on text input focus/blur.
    • Applied animated width and opacity styles to the chat input icons container.
    • Set pointerEvents='none' on focused icons to prevent interaction.
  • src/components/ChatInput/styles.ts
    • Added overflow: 'hidden' to the chat input pill style to clip collapsing icons.
  • src/screens/ModelsScreen/index.tsx
    • Integrated a CustomAlert component to display a notification permission rationale to the user.
  • src/screens/ModelsScreen/useModelsScreen.ts
    • Introduced state and refs to manage notification rationale display.
    • Implemented maybeShowNotifRationale to conditionally prompt for POST_NOTIFICATIONS permission on Android 13+ for the first download.
    • Wrapped handleDownload and handleDownloadImageModel to include the notification rationale check.
    • Added handlers for allowing and dismissing the notification rationale.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a nice animation to the chat input, collapsing the side icons on focus to provide more typing space. The implementation using React Native's Animated API is well-executed. I've left one minor suggestion to improve maintainability by replacing a magic number with constants.

Additionally, the refactoring of the notification permission logic is a great improvement. Moving it from App.tsx to the ModelsScreen and adding a user-friendly rationale dialog before the system prompt is a much better user experience.

Comment thread src/components/ChatInput/index.tsx Outdated
<Animated.View
pointerEvents={isFocused ? 'none' : 'auto'}
style={[styles.pillIcons, {
width: focusAnim.interpolate({ inputRange: [0, 1], outputRange: [108, 0] }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The hardcoded value 108 for the width animation is a magic number. It's derived from the width of the three icons (3 * 36px). To improve maintainability, consider defining constants for these values outside the component and using them here. This will make it easier to update if an icon is added/removed or the icon size changes.

For example:

const PILL_ICON_WIDTH = 36; // From styles.ts pillIconButton.width
const NUM_PILL_ICONS = 3;
const ICONS_CONTAINER_WIDTH = PILL_ICON_WIDTH * NUM_PILL_ICONS;

// ... inside the component's style
width: focusAnim.interpolate({ inputRange: [0, 1], outputRange: [ICONS_CONTAINER_WIDTH, 0] }),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call — extracted PILL_ICON_SIZE and PILL_ICONS_WIDTH constants in styles.ts and used them both in the style definition and the animation interpolation. Fixed in 39278a3.

github-actions Bot and others added 3 commits February 23, 2026 23:03
PermissionsAndroid.request() was passed a rationale object which caused
Android to show its own alert dialog before the OS system prompt. Since
we already show a custom bottom sheet rationale, this intermediate dialog
was redundant — resulting in 3 dialogs for a single permission. Now the
flow is: bottom sheet rationale → OS system dialog (2 steps).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded 108 with PILL_ICONS_WIDTH (PILL_ICON_SIZE * 3) derived
from the same constant used in the style definition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract notification rationale logic into useNotifRationale hook to
  bring useModelsScreen under the 250-line function limit
- Remove unused backgroundDownloadService import from App.tsx
- Remove unused logger import from useModelsScreen.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alichherawalla

Copy link
Copy Markdown
Collaborator Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a nice animation for the chat input icons on focus, improving the user experience by providing more space for typing. The implementation using Animated.View is well-done. Additionally, the refactoring of the notification permission request to be more contextual is a great improvement. I have one suggestion to improve maintainability by extracting hardcoded animation durations into constants.

Comment thread src/components/ChatInput/index.tsx Outdated
Comment on lines +56 to +64
const handleFocus = useCallback(() => {
setIsFocused(true);
Animated.timing(focusAnim, { toValue: 1, duration: 180, useNativeDriver: false }).start();
}, [focusAnim]);

const handleBlur = useCallback(() => {
setIsFocused(false);
Animated.timing(focusAnim, { toValue: 0, duration: 200, useNativeDriver: false }).start();
}, [focusAnim]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The animation durations 180 and 200 are hardcoded. To improve maintainability and consistency, it's better to extract these magic numbers into named constants.

For example, you could define them in src/components/ChatInput/styles.ts and import them:

// in styles.ts
export const ANIMATION_DURATION_IN = 180;
export const ANIMATION_DURATION_OUT = 200;

Then use them here:

// in index.tsx
import { ..., ANIMATION_DURATION_IN, ANIMATION_DURATION_OUT } from './styles';
// ...
Animated.timing(focusAnim, { toValue: 1, duration: ANIMATION_DURATION_IN, useNativeDriver: false }).start();
// ...
Animated.timing(focusAnim, { toValue: 0, duration: ANIMATION_DURATION_OUT, useNativeDriver: false }).start();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — extracted ANIM_DURATION_IN (180ms) and ANIM_DURATION_OUT (200ms) constants in styles.ts.

github-actions Bot and others added 3 commits February 23, 2026 23:23
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Width collapse alone just clips the icons. Adding translateX pushes them
rightward so they visually slide out of the pill on focus.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop the translateX approach — it fights with overflow:hidden on the
pill. Width collapse from PILL_ICONS_WIDTH→0 with a fast opacity fade
(0→0.4 of the animation) gives a clean disappearing effect. The pill's
overflow:hidden handles clipping naturally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@alichherawalla

Copy link
Copy Markdown
Collaborator Author

/gemini review

ChatInput (7 new):
- Animated.timing fires on focus/blur
- pointerEvents disabled/re-enabled on focus/blur
- Icons accessible when unfocused
- Send and stop buttons remain visible during focus

useNotifRationale (7 new):
- Skips rationale when not first download, on iOS, on API < 33
- Skips when permission already granted
- Shows rationale on Android 33+ first download
- Allow triggers permission request then proceeds
- Dismiss proceeds without requesting permission

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request successfully implements the animation for chat input icons and introduces a notification rationale flow for Android 13+. The animation logic is sound, using Animated.timing with useNativeDriver: false to handle width changes. The notification rationale improves UX by explaining why permissions are needed before showing the system prompt. However, there is a minor UX issue where the rationale alert can appear multiple times if the user hasn't completed their first download yet. Additionally, a suggestion was made to ensure overflow: 'hidden' is applied directly to an Animated.View for safer clipping.

Comment on lines +10 to +24
const maybeShowNotifRationale = useCallback(async (proceed: () => void) => {
if (Platform.OS !== 'android' || Platform.Version < 33 || !isFirstDownload) {
proceed();
return;
}
const alreadyGranted = await PermissionsAndroid.check(
PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS,
);
if (alreadyGranted) {
proceed();
return;
}
pendingDownload.current = proceed;
setShowNotifRationale(true);
}, [isFirstDownload]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The rationale alert will be shown every time a download is initiated as long as isFirstDownload remains true (i.e., until the first model is fully downloaded and added to the list). If a user starts multiple downloads in quick succession or dismisses the rationale once, they will be repeatedly prompted. Consider using a useRef or state variable to track if the rationale has already been shown in the current session to avoid redundant prompts.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — added a hasShownRationale ref that gets set to true on first show. Subsequent calls skip the rationale and proceed directly. Fixed in 299e6e0.

Comment on lines +168 to +174
<Animated.View
pointerEvents={isFocused ? 'none' : 'auto'}
style={[styles.pillIcons, {
width: focusAnim.interpolate({ inputRange: [0, 1], outputRange: [PILL_ICONS_WIDTH, 0] }),
opacity: focusAnim.interpolate({ inputRange: [0, 0.4], outputRange: [1, 0], extrapolate: 'clamp' }),
}]}
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While overflow: 'hidden' is applied to the parent pill container, it is safer to also apply it directly to this Animated.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 the TextInput or other elements if the parent's clipping doesn't align perfectly with the shrinking container.

          <Animated.View
            pointerEvents={isFocused ? 'none' : 'auto'}
            style={[
              styles.pillIcons,
              {
                width: focusAnim.interpolate({ inputRange: [0, 1], outputRange: [PILL_ICONS_WIDTH, 0] }),
                opacity: focusAnim.interpolate({ inputRange: [0, 0.4], outputRange: [1, 0], extrapolate: 'clamp' }),
                overflow: 'hidden'
              }
            ]}
          >

Copy link
Copy Markdown
Collaborator Author

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 icons Animated.View for safer clipping. Fixed in 299e6e0.

github-actions Bot and others added 2 commits February 23, 2026 23:50
… clip

- Add hasShownRationale ref to prevent repeated prompts when user
  dismisses and starts another download before the first completes
- Add overflow:'hidden' directly on the icons Animated.View for safer
  clipping during width collapse
- Add test for once-per-session behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Icons now hide when user types any character and reappear when input is
empty, providing a more intuitive experience than focus-based animation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@alichherawalla
alichherawalla merged commit 37a0a79 into main Feb 23, 2026
5 checks passed
@codecov

codecov Bot commented Feb 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.34884% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.00%. Comparing base (16b4994) to head (937254f).
⚠️ Report is 14 commits behind head on main.

Files with missing lines Patch % Lines
src/screens/ModelsScreen/useModelsScreen.ts 83.33% 1 Missing ⚠️
src/screens/ModelsScreen/useNotifRationale.ts 96.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #72      +/-   ##
==========================================
+ Coverage   87.95%   88.00%   +0.04%     
==========================================
  Files         149      150       +1     
  Lines        6436     6479      +43     
  Branches     1784     1790       +6     
==========================================
+ Hits         5661     5702      +41     
- Misses        411      413       +2     
  Partials      364      364              
Files with missing lines Coverage Δ
src/components/ChatInput/index.tsx 95.71% <100.00%> (+0.47%) ⬆️
src/components/ChatInput/styles.ts 75.00% <100.00%> (+41.66%) ⬆️
src/screens/ModelsScreen/index.tsx 91.30% <ø> (ø)
src/services/backgroundDownloadService.ts 93.98% <ø> (ø)
src/screens/ModelsScreen/useModelsScreen.ts 36.36% <83.33%> (+3.43%) ⬆️
src/screens/ModelsScreen/useNotifRationale.ts 96.00% <96.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant