mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
feat: offer the code of sign-in and verification mails for copying in the list and the reader
This commit is contained in:
@@ -19,6 +19,8 @@ import {
|
||||
import { formatSize } from '../../lib/attachment-display';
|
||||
import { isSmimeEmail } from '../../lib/smime';
|
||||
import { resolveEmailTags } from '../../lib/thread-utils';
|
||||
import { findVerificationCode, verificationCodeBodyText } from '../../lib/verification-code';
|
||||
import { VerificationCodeChip } from './VerificationCodeChip';
|
||||
|
||||
interface Props {
|
||||
email: Email;
|
||||
@@ -99,6 +101,7 @@ export function MessageHeader({ email, identities, headerInfo, onToggleStar, onA
|
||||
const locale = useLocaleStore((s) => s.locale);
|
||||
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
||||
const keywordDefs = useKeywordsStore((s) => s.keywords);
|
||||
const showVerificationCodes = useSettingsStore((s) => s.showVerificationCodes);
|
||||
const [showDetails, setShowDetails] = React.useState(false);
|
||||
React.useEffect(() => { setShowDetails(false); }, [email.id]);
|
||||
|
||||
@@ -114,6 +117,15 @@ export function MessageHeader({ email, identities, headerInfo, onToggleStar, onA
|
||||
[info.auth, from?.email],
|
||||
);
|
||||
const verificationText = verification ? describeSenderVerification(verification, t) : null;
|
||||
// The sign-in code of the message, offered for copying. Looked up again
|
||||
// only when the message or its loaded body changes, not on a keyword change.
|
||||
const verificationCode = React.useMemo(
|
||||
() => (showVerificationCodes && !compact
|
||||
? findVerificationCode(email.subject, verificationCodeBodyText(email))
|
||||
: null),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[showVerificationCodes, compact, email.id, email.subject, email.bodyValues],
|
||||
);
|
||||
const verificationColor = verification?.status === 'failed' ? c.error : c.warning;
|
||||
const verificationBg = verification?.status === 'failed' ? c.errorBg : c.warningBg;
|
||||
|
||||
@@ -253,6 +265,8 @@ export function MessageHeader({ email, identities, headerInfo, onToggleStar, onA
|
||||
</View>
|
||||
)}
|
||||
|
||||
{verificationCode && <VerificationCodeChip code={verificationCode} />}
|
||||
|
||||
{!compact && verificationText && (
|
||||
<View style={[styles.verificationNotice, { borderColor: verificationColor }]} accessibilityRole="alert">
|
||||
<ShieldAlert size={18} color={verificationColor} />
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
import * as Clipboard from 'expo-clipboard';
|
||||
import { Check, Key } from '../icons';
|
||||
import type { Email } from '../../api/types';
|
||||
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { useSettingsStore } from '../../stores/settings-store';
|
||||
import { toast } from '../../stores/toast-store';
|
||||
import { listVerificationCode } from '../../lib/verification-code';
|
||||
|
||||
/** The code a list row offers: found in the subject and preview of a fresh mail. */
|
||||
export function useListVerificationCode(email: Pick<Email, 'subject' | 'preview' | 'receivedAt'>): string | null {
|
||||
const enabled = useSettingsStore((s) => s.showVerificationCodes);
|
||||
const { subject, preview, receivedAt } = email;
|
||||
return React.useMemo(
|
||||
() => (enabled ? listVerificationCode({ subject, preview, receivedAt }) : null),
|
||||
[enabled, subject, preview, receivedAt],
|
||||
);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
code: string;
|
||||
/** A list row in selection mode: the tap selects the row instead. */
|
||||
disabled?: boolean;
|
||||
/** Spacing for a list row (below the preview). */
|
||||
inList?: boolean;
|
||||
}
|
||||
|
||||
/** The one-time code of a sign-in mail (lib/verification-code.ts); a tap copies it. */
|
||||
export function VerificationCodeChip({ code, disabled, inList }: Props) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!copied) return undefined;
|
||||
const timer = setTimeout(() => setCopied(false), 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [copied]);
|
||||
|
||||
const copy = () => {
|
||||
Clipboard.setStringAsync(code).then(
|
||||
() => {
|
||||
setCopied(true);
|
||||
toast.success(t('email_viewer.verification_code.copied', 'Code copied'));
|
||||
},
|
||||
() => toast.error(t('email_viewer.verification_code.copy_failed', 'Could not copy the code')),
|
||||
);
|
||||
};
|
||||
|
||||
const Icon = copied ? Check : Key;
|
||||
const label = t('email_viewer.verification_code.copy', 'Copy code {code}', { code });
|
||||
return (
|
||||
<View style={[styles.row, inList && styles.rowInList]}>
|
||||
<Pressable
|
||||
onPress={copy}
|
||||
disabled={disabled}
|
||||
hitSlop={6}
|
||||
style={({ pressed }) => [styles.chip, pressed && styles.chipPressed]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={label}
|
||||
testID="verification-code-chip"
|
||||
>
|
||||
<Icon size={12} color={copied ? c.success : c.warning} />
|
||||
<Text style={styles.code} numberOfLines={1}>{code}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function makeStyles(c: ThemePalette) {
|
||||
return StyleSheet.create({
|
||||
row: { flexDirection: 'row' },
|
||||
rowInList: { marginTop: spacing.xs },
|
||||
chip: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 3,
|
||||
borderWidth: 1,
|
||||
borderColor: c.border,
|
||||
borderRadius: radius.sm,
|
||||
backgroundColor: c.surface,
|
||||
},
|
||||
chipPressed: { backgroundColor: c.surfaceHover },
|
||||
code: { ...typography.caption, color: c.text, fontWeight: '600', fontFamily: 'monospace', letterSpacing: 0.5 },
|
||||
});
|
||||
}
|
||||
@@ -32,6 +32,7 @@ export function ReadingSettings() {
|
||||
const deleteAction = useSettingsStore((s) => s.deleteAction);
|
||||
const permanentlyDeleteJunk = useSettingsStore((s) => s.permanentlyDeleteJunk);
|
||||
const showPreview = useSettingsStore((s) => s.showPreview);
|
||||
const showVerificationCodes = useSettingsStore((s) => s.showVerificationCodes);
|
||||
const clearSearchOnFolderChange = useSettingsStore((s) => s.clearSearchOnFolderChange);
|
||||
const disableThreading = useSettingsStore((s) => s.disableThreading);
|
||||
const includeGroupInUnified = useSettingsStore((s) => s.includeGroupInUnified);
|
||||
@@ -193,6 +194,10 @@ export function ReadingSettings() {
|
||||
<ToggleSwitch checked={showPreview} onChange={(v) => update('showPreview', v)} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('settings.email_behavior.show_verification_codes.label', "Show Verification Codes")} description={t('settings.email_behavior.show_verification_codes.description', "Offer the code from sign-in and confirmation emails for one-click copying, in the list for a day and in the message")}>
|
||||
<ToggleSwitch checked={showVerificationCodes} onChange={(v) => update('showVerificationCodes', v)} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('settings.email_behavior.disable_threading.label', "Disable Conversation Grouping")} description={t('settings.email_behavior.disable_threading.description', "Show emails as individual messages instead of grouped by conversation")}>
|
||||
<ToggleSwitch checked={disableThreading} onChange={(v) => update('disableThreading', v)} />
|
||||
</SettingItem>
|
||||
|
||||
Reference in New Issue
Block a user