mattermost-mobile/app/components/floating_banner/banner_section.tsx
Rahim Rahman 5887c3ab46
feat(MM-65625): floating banner (#9162)
* feat: implement floating banner system

- Add FloatingBanner component with gesture support and keyboard awareness
- Implement BannerManager singleton for banner lifecycle management
- Create floating banner screen with SafeAreaProvider integration
- Add comprehensive banner configuration types and positioning
- Update Banner component to use modern gesture handling
- Enhance BannerItem with improved typography and spacing (40px height)
- Add ConnectionBanner improvements with better sizing
- Remove ConnectionBanner from channel list (moved to floating system)
- Update screens constants (remove FLOATING_BANNER - handled as overlay)
- Add i18n support for limited network connection message

The system provides:
- Auto-hide functionality with customizable duration
- Position-aware rendering (top/bottom with keyboard adjustment)
- Tablet-specific offset handling
- Swipe-to-dismiss with configurable thresholds
- Custom component support alongside default banner items
- Comprehensive test coverage with device-specific scenarios

* docs: add floating banner system documentation and cleanup

- Add comprehensive floating-banner.md with architecture diagrams
- Remove incompatible connection_banner/index.ts file
- Update device.ts hooks for better keyboard handling
- Simplify screens/index.tsx floating banner registration
- Update test/setup.ts to remove deprecated keyboard mocks
- Clean up keyboard height logic and ESLint issues

The documentation covers:
- System architecture and component relationships
- API reference and usage patterns
- Performance considerations and best practices
- Integration points and troubleshooting guide
- Comprehensive testing strategy

All tests now pass with the updated setup.

* fix issue with translation file

* some self cleanup.

* renamed index.tsx => Banner.tsx

* creaete meaningful tests for Banner component and all the hooks.

* fix tests

* cleanup based on initial review by AI

* dismissible was set to true, changing to what was configured.

* making title and message optional

* addressed some comments in PR

* more fixes based on PR review.

* added future enhancement

* dismissOverlay will be awaited
* delay dismissing overlay so we don't have to show a new one all the time

* make the banner stackable

* Fix issue with last banner dismissal delayed by 2s

* update floating-banner test

* clean-up based on review by @enahum

* fix failing test

* fix failiing tests

* rename confusing var

* fixed issue with swipe not working on android

* fix issue w/ android not registering touch events behind the overlay

* fix failing test

* animate the banner moving up when bottom banner first appear.

* removed unused functions and update tests

* add useMemo and useCallback

* update jsdoc to say dismissable is default true

* fix failing test
2025-10-12 09:33:51 -06:00

109 lines
3.2 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useMemo} from 'react';
import {StyleSheet} from 'react-native';
import {GestureHandlerRootView} from 'react-native-gesture-handler';
import Animated, {useAnimatedStyle} from 'react-native-reanimated';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import {
BANNER_SPACING,
} from '@constants/view';
import {useBannerGestureRootPosition} from '@hooks/useBannerGestureRootPosition';
import AnimatedBannerItem from './animated_banner_item';
import type {FloatingBannerConfig, FloatingBannerPosition} from './types';
type BannerSectionProps = {
sectionBanners: FloatingBannerConfig[];
position: FloatingBannerPosition;
onBannerPress: (banner: FloatingBannerConfig) => void;
onBannerDismiss: (banner: FloatingBannerConfig) => void;
};
const BANNER_HEIGHT = 40;
const styles = StyleSheet.create({
gestureRoot: {
position: 'absolute',
width: '100%',
},
containerBase: {
width: '100%',
paddingHorizontal: 16,
alignItems: 'center',
},
topContainer: {
top: 0,
paddingTop: 8,
},
bottomContainer: {
paddingBottom: 8,
},
});
const BannerSection: React.FC<BannerSectionProps> = ({
sectionBanners,
position,
onBannerPress,
onBannerDismiss,
}) => {
if (!sectionBanners.length) {
return null;
}
const insets = useSafeAreaInsets();
const isTop = position === 'top';
const testID = isTop ? 'floating-banner-top-container' : 'floating-banner-bottom-container';
const containerStyle = isTop ? styles.topContainer :styles.bottomContainer;
const animatedStyle = useAnimatedStyle(() => {
if (isTop) {
return {paddingTop: insets.top + 8};
}
return {};
}, [isTop, insets.top]);
// Limitation: Assumes all banners have the same fixed height (BANNER_HEIGHT).
// If banners have different heights, the gesture area may not align perfectly.
// Future improvement: Measure actual banner heights using onLayout for dynamic sizing.
const containerHeight = useMemo(() => {
const numBanners = sectionBanners.length;
const spacing = (numBanners - 1) * BANNER_SPACING;
return (BANNER_HEIGHT * numBanners) + spacing;
}, [sectionBanners.length]);
const gestureRootStyle = useBannerGestureRootPosition({
position,
containerHeight,
});
return (
<GestureHandlerRootView
style={[styles.gestureRoot, gestureRootStyle]}
pointerEvents='box-none'
>
<Animated.View
testID={testID}
style={[styles.containerBase, containerStyle, animatedStyle]}
>
{sectionBanners.map((banner, index) => (
<AnimatedBannerItem
key={banner.id}
banner={banner}
index={index}
onBannerPress={onBannerPress}
onBannerDismiss={onBannerDismiss}
/>
))}
</Animated.View>
</GestureHandlerRootView>
);
};
export default BannerSection;