mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-04-05 07:14:15 -04:00
Add clipboard countdown context to keep global track of copied field id (#881)
This commit is contained in:
@@ -79,6 +79,8 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
display = t('settings.clipboardClearOptions.10seconds');
|
||||
} else if (clipboardTimeout === 15) {
|
||||
display = t('settings.clipboardClearOptions.15seconds');
|
||||
} else if (clipboardTimeout === 30) {
|
||||
display = t('settings.clipboardClearOptions.30seconds');
|
||||
}
|
||||
|
||||
setClipboardClearDisplay(display);
|
||||
@@ -101,7 +103,7 @@ export default function SettingsScreen() : React.ReactNode {
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [getAutoLockTimeout, getAuthMethodDisplayKey, setIsFirstLoad, loadApiUrl, t])
|
||||
}, [getAutoLockTimeout, getAuthMethodDisplayKey, setIsFirstLoad, loadApiUrl, getClipboardClearTimeout, t])
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,7 @@ import SpaceMono from '@/assets/fonts/SpaceMono-Regular.ttf';
|
||||
import { ThemedView } from '@/components/themed/ThemedView';
|
||||
import { AliasVaultToast } from '@/components/Toast';
|
||||
import { AuthProvider } from '@/context/AuthContext';
|
||||
import { ClipboardCountdownProvider } from '@/context/ClipboardCountdownContext';
|
||||
import { DbProvider } from '@/context/DbContext';
|
||||
import { WebApiProvider } from '@/context/WebApiContext';
|
||||
import { initI18n } from '@/i18n';
|
||||
@@ -182,7 +183,9 @@ export default function RootLayout() : React.ReactNode {
|
||||
<DbProvider>
|
||||
<AuthProvider>
|
||||
<WebApiProvider>
|
||||
<RootLayoutNav />
|
||||
<ClipboardCountdownProvider>
|
||||
<RootLayoutNav />
|
||||
</ClipboardCountdownProvider>
|
||||
</WebApiProvider>
|
||||
</AuthProvider>
|
||||
</DbProvider>
|
||||
|
||||
@@ -8,6 +8,7 @@ import Toast from 'react-native-toast-message';
|
||||
|
||||
import { useColors } from '@/hooks/useColorScheme';
|
||||
|
||||
import { useClipboardCountdown } from '@/context/ClipboardCountdownContext';
|
||||
import NativeVaultManager from '@/specs/NativeVaultManager';
|
||||
|
||||
type FormInputCopyToClipboardProps = {
|
||||
@@ -27,16 +28,50 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
const colors = useColors();
|
||||
const { t } = useTranslation();
|
||||
const { activeFieldId, setActiveField } = useClipboardCountdown();
|
||||
|
||||
const animatedWidth = useRef(new Animated.Value(0)).current;
|
||||
const [isCountingDown, setIsCountingDown] = useState(false);
|
||||
// Create a stable unique ID based on label and value
|
||||
const fieldId = useRef(`${label}-${value}-${Math.random().toString(36).substring(2, 11)}`).current;
|
||||
const isCountingDown = activeFieldId === fieldId;
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
return (): void => {
|
||||
// Cleanup on unmount
|
||||
animatedWidth.stopAnimation();
|
||||
};
|
||||
}, [animatedWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
/* Handle animation based on whether this field is active */
|
||||
if (isCountingDown) {
|
||||
// This field is now active - reset and start animation
|
||||
animatedWidth.stopAnimation();
|
||||
animatedWidth.setValue(100);
|
||||
|
||||
// Get timeout and start animation
|
||||
AsyncStorage.getItem('clipboard_clear_timeout').then((timeoutStr) => {
|
||||
const timeoutSeconds = timeoutStr ? parseInt(timeoutStr, 10) : 10;
|
||||
if (timeoutSeconds > 0 && activeFieldId === fieldId) {
|
||||
Animated.timing(animatedWidth, {
|
||||
toValue: 0,
|
||||
duration: timeoutSeconds * 1000,
|
||||
useNativeDriver: false,
|
||||
easing: Easing.linear,
|
||||
}).start((finished) => {
|
||||
if (finished && activeFieldId === fieldId) {
|
||||
setActiveField(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// This field is not active - stop animation and reset
|
||||
animatedWidth.stopAnimation();
|
||||
animatedWidth.setValue(0);
|
||||
}
|
||||
}, [isCountingDown, activeFieldId, fieldId, animatedWidth, setActiveField]);
|
||||
|
||||
/**
|
||||
* Copy the value to the clipboard.
|
||||
*/
|
||||
@@ -52,20 +87,19 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
|
||||
|
||||
// Schedule clipboard clear if timeout is set
|
||||
if (timeoutSeconds > 0) {
|
||||
// Clear any existing active field first (this will cancel its animation)
|
||||
setActiveField(null);
|
||||
|
||||
// Schedule the clipboard clear
|
||||
await NativeVaultManager.clearClipboardAfterDelay(timeoutSeconds);
|
||||
|
||||
// Start countdown animation
|
||||
setIsCountingDown(true);
|
||||
animatedWidth.setValue(100);
|
||||
|
||||
Animated.timing(animatedWidth, {
|
||||
toValue: 0,
|
||||
duration: timeoutSeconds * 1000,
|
||||
useNativeDriver: false,
|
||||
easing: Easing.linear,
|
||||
}).start(() => {
|
||||
setIsCountingDown(false);
|
||||
});
|
||||
/*
|
||||
* Now set this field as active - animation will be handled by the effect
|
||||
* Use setTimeout to ensure state update happens in next tick
|
||||
*/
|
||||
setTimeout(() => {
|
||||
setActiveField(fieldId);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
if (Platform.OS !== 'android') {
|
||||
@@ -98,6 +132,14 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
animatedOverlay: {
|
||||
backgroundColor: `${colors.primary}50`,
|
||||
borderRadius: 8,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
},
|
||||
iconButton: {
|
||||
padding: 8,
|
||||
},
|
||||
@@ -105,12 +147,14 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
|
||||
backgroundColor: colors.accentBackground,
|
||||
borderRadius: 8,
|
||||
marginBottom: 8,
|
||||
padding: 12,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
},
|
||||
inputContent: {
|
||||
alignItems: 'center',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
padding: 12,
|
||||
},
|
||||
label: {
|
||||
color: colors.textMuted,
|
||||
@@ -122,14 +166,6 @@ const FormInputCopyToClipboard: React.FC<FormInputCopyToClipboardProps> = ({
|
||||
fontSize: 16,
|
||||
fontWeight: '500',
|
||||
},
|
||||
animatedOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: `${colors.primary}50`,
|
||||
borderRadius: 8,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
42
apps/mobile-app/context/ClipboardCountdownContext.tsx
Normal file
42
apps/mobile-app/context/ClipboardCountdownContext.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { createContext, useContext, useState, useCallback, useMemo } from 'react';
|
||||
|
||||
type ClipboardCountdownContextType = {
|
||||
activeFieldId: string | null;
|
||||
setActiveField: (fieldId: string | null | ((prev: string | null) => string | null)) => void;
|
||||
}
|
||||
|
||||
const ClipboardCountdownContext = createContext<ClipboardCountdownContextType | undefined>(undefined);
|
||||
|
||||
/**
|
||||
* Clipboard countdown context provider.
|
||||
*/
|
||||
export const ClipboardCountdownProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [activeFieldId, setActiveFieldId] = useState<string | null>(null);
|
||||
|
||||
const setActiveField = useCallback((fieldId: string | null | ((prev: string | null) => string | null)) => {
|
||||
if (typeof fieldId === 'function') {
|
||||
setActiveFieldId(fieldId);
|
||||
} else {
|
||||
setActiveFieldId(fieldId);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const value = useMemo(() => ({ activeFieldId, setActiveField }), [activeFieldId, setActiveField]);
|
||||
|
||||
return (
|
||||
<ClipboardCountdownContext.Provider value={value}>
|
||||
{children}
|
||||
</ClipboardCountdownContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Clipboard countdown context hook.
|
||||
*/
|
||||
export const useClipboardCountdown = (): ClipboardCountdownContextType => {
|
||||
const context = useContext(ClipboardCountdownContext);
|
||||
if (!context) {
|
||||
throw new Error('useClipboardCountdown must be used within ClipboardCountdownProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -199,7 +199,8 @@
|
||||
"never": "Never",
|
||||
"5seconds": "5 seconds",
|
||||
"10seconds": "10 seconds",
|
||||
"15seconds": "15 seconds"
|
||||
"15seconds": "15 seconds",
|
||||
"30seconds": "30 seconds"
|
||||
},
|
||||
"identityGenerator": "Identity Generator",
|
||||
"security": "Security",
|
||||
|
||||
Reference in New Issue
Block a user