mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
fix: hide reply and forward on drafts in a conversation
This commit is contained in:
@@ -13,6 +13,7 @@ import { singleLine } from '../../lib/single-line';
|
||||
import { cleanPreview } from '../../lib/preview-text';
|
||||
import { useSenderWarning } from './use-sender-warning';
|
||||
import { useOwnDomainAddress } from '../../lib/use-own-domain-address';
|
||||
import type { CardReplyActions } from '../../lib/thread-card-actions';
|
||||
|
||||
interface Props extends MessageContentProps {
|
||||
expanded: boolean;
|
||||
@@ -24,8 +25,11 @@ interface Props extends MessageContentProps {
|
||||
* the card so the content survives a conversation arriving around it.
|
||||
*/
|
||||
bare?: boolean;
|
||||
/** The full message is here (not only its header), so it can be replied to. */
|
||||
replyable?: boolean;
|
||||
/**
|
||||
* The reply row: hidden on a draft or scheduled message, disabled until
|
||||
* the full message is here (lib/thread-card-actions).
|
||||
*/
|
||||
replyActions?: CardReplyActions;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,7 +38,7 @@ interface Props extends MessageContentProps {
|
||||
* actions - the webmail's thread-conversation-view cards.
|
||||
*/
|
||||
export function ThreadMessageCard({
|
||||
expanded, onToggleExpanded, onReply, bare, replyable = true, ...content
|
||||
expanded, onToggleExpanded, onReply, bare, replyActions = 'enabled', ...content
|
||||
}: Props) {
|
||||
const { email, onToggleStar } = content;
|
||||
const c = useColors();
|
||||
@@ -71,24 +75,29 @@ export function ThreadMessageCard({
|
||||
|
||||
// The content keeps its place among the children whether bare or not, so
|
||||
// it (and its WebView) is not remounted when a conversation arrives.
|
||||
const replyable = replyActions === 'enabled';
|
||||
const reply = (mode: 'reply' | 'replyAll' | 'forward') => (replyable ? () => onReply(mode, email) : undefined);
|
||||
const actionA11y = {
|
||||
accessibilityRole: 'button' as const,
|
||||
accessibilityState: { disabled: !replyable },
|
||||
};
|
||||
return (
|
||||
<View style={bare ? (content.fill ? styles.bareFill : undefined) : styles.expanded}>
|
||||
{!bare && (
|
||||
<Pressable onPress={onToggleExpanded} style={styles.collapseHandle} hitSlop={6} accessibilityLabel={t('threads.collapse', 'Collapse conversation')} />
|
||||
)}
|
||||
<MessageContent {...content} compact={false} onToggleStar={bare ? undefined : onToggleStar} />
|
||||
{!bare && (
|
||||
{!bare && replyActions !== 'hidden' && (
|
||||
<View style={[styles.actions, !replyable && styles.actionsDisabled]}>
|
||||
<Pressable style={styles.actionBtn} onPress={reply('reply')} hitSlop={6}>
|
||||
<Pressable style={styles.actionBtn} onPress={reply('reply')} disabled={!replyable} hitSlop={6} {...actionA11y}>
|
||||
<Reply size={16} color={c.text} />
|
||||
<Text style={styles.actionLabel}>{t('email_viewer.reply', 'Reply')}</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.actionBtn} onPress={reply('replyAll')} hitSlop={6}>
|
||||
<Pressable style={styles.actionBtn} onPress={reply('replyAll')} disabled={!replyable} hitSlop={6} {...actionA11y}>
|
||||
<ReplyAll size={16} color={c.text} />
|
||||
<Text style={styles.actionLabel}>{t('email_viewer.reply_all', 'Reply All')}</Text>
|
||||
</Pressable>
|
||||
<Pressable style={styles.actionBtn} onPress={reply('forward')} hitSlop={6}>
|
||||
<Pressable style={styles.actionBtn} onPress={reply('forward')} disabled={!replyable} hitSlop={6} {...actionA11y}>
|
||||
<Forward size={16} color={c.text} />
|
||||
<Text style={styles.actionLabel}>{t('email_viewer.forward', 'Forward')}</Text>
|
||||
</Pressable>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { cardReplyActions } from '../thread-card-actions';
|
||||
|
||||
// A draft in a conversation showed Reply / Reply all / Forward greyed out,
|
||||
// announced to screen readers as enabled buttons. The webmail renders no
|
||||
// reply buttons on a draft or a scheduled message; neither does the card.
|
||||
|
||||
const read = (rel: string) => fs.readFileSync(path.resolve(__dirname, '..', '..', rel), 'utf8');
|
||||
const NONE = new Set<string>();
|
||||
|
||||
describe('reply row of a conversation card', () => {
|
||||
it('hides the row on a draft and on a scheduled message', () => {
|
||||
expect(cardReplyActions({ id: 'd1', keywords: { $draft: true } }, true, NONE)).toBe('hidden');
|
||||
expect(cardReplyActions({ id: 'd1', keywords: { $draft: true } }, false, NONE)).toBe('hidden');
|
||||
expect(cardReplyActions({ id: 's1', keywords: {} }, true, new Set(['s1']))).toBe('hidden');
|
||||
});
|
||||
|
||||
it('shows it disabled while the body loads, then enabled', () => {
|
||||
expect(cardReplyActions({ id: 'm1', keywords: { $seen: true } }, false, NONE)).toBe('disabled');
|
||||
expect(cardReplyActions({ id: 'm1', keywords: { $seen: true } }, true, NONE)).toBe('enabled');
|
||||
});
|
||||
|
||||
it('renders no row for a hidden state and marks disabled buttons for screen readers', () => {
|
||||
const card = read('components/email/ThreadMessageCard.tsx');
|
||||
expect(card).toMatch(/\{!bare && replyActions !== 'hidden' && \(/);
|
||||
expect(card).toMatch(/accessibilityState: \{ disabled: !replyable \}/);
|
||||
expect(read('screens/EmailThreadScreen.tsx')).toMatch(/replyActions=\{cardReplyActions\(m, !!full, scheduledIds\)\}/);
|
||||
});
|
||||
});
|
||||
@@ -33,7 +33,7 @@ describe('replying in a conversation (bulwarkmail/webmail#1404)', () => {
|
||||
|
||||
it('is wired into the conversation cards and the quick reply', () => {
|
||||
const screen = fs.readFileSync(path.resolve(__dirname, '../../screens/EmailThreadScreen.tsx'), 'utf8');
|
||||
expect(screen).toMatch(/replyable=\{!!full && isReplyableMember\(m, scheduledIds\)\}/);
|
||||
expect(screen).toMatch(/replyActions=\{cardReplyActions\(m, !!full, scheduledIds\)\}/);
|
||||
expect(screen).toMatch(/const newest = quickReplyTarget\(/);
|
||||
expect(screen).toMatch(/scheduledEmailIdsInThread\(threadId, jmapAccountId\)/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// The Reply / Reply all / Forward row under an open message of a
|
||||
// conversation. A draft or a scheduled message gets none (webmail
|
||||
// email-viewer.tsx: the reply buttons render only `!isDraft && !isScheduled`);
|
||||
// greyed-out buttons that a screen reader announced as enabled were left on
|
||||
// drafts. A message whose body is still loading shows them disabled.
|
||||
|
||||
import type { Email } from '../api/types';
|
||||
import { isReplyableMember } from './thread-utils';
|
||||
|
||||
export type CardReplyActions = 'hidden' | 'disabled' | 'enabled';
|
||||
|
||||
export function cardReplyActions(
|
||||
email: Pick<Email, 'id' | 'keywords'>,
|
||||
loaded: boolean,
|
||||
scheduledIds: ReadonlySet<string>,
|
||||
): CardReplyActions {
|
||||
if (!isReplyableMember(email, scheduledIds)) return 'hidden';
|
||||
return loaded ? 'enabled' : 'disabled';
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { pickEmailBody } from '../lib/email-body';
|
||||
import { confirmMailRemoval } from '../lib/mail-removal-confirm';
|
||||
import { useTagUndoGroup } from '../lib/tag-undo-group';
|
||||
import { cardReplyActions } from '../lib/thread-card-actions';
|
||||
import { NewTagRow } from '../components/NewTagRow';
|
||||
import { QuickRuleSheet } from '../components/filters/QuickRuleSheet';
|
||||
import { isSieveSupported } from '../api/sieve';
|
||||
@@ -51,7 +52,7 @@ import { replyComposeParams } from '../lib/reply-compose';
|
||||
import { singleLine } from '../lib/single-line';
|
||||
import { buildForwardAsAttachmentPayload } from '../lib/forward-as-attachment';
|
||||
import { viewerInstance, viewerPages, type ViewerInstance } from '../lib/viewer-pages';
|
||||
import { accountScopedId, isReplyableMember, quickReplyTarget } from '../lib/thread-utils';
|
||||
import { accountScopedId, quickReplyTarget } from '../lib/thread-utils';
|
||||
import { scheduledEmailIdsInThread } from '../api/thread-scheduled';
|
||||
import type { Email, EmailAddress, Identity } from '../api/types';
|
||||
import type { RootStackParamList } from '../navigation/types';
|
||||
@@ -1260,7 +1261,7 @@ function EmailPane({
|
||||
fill={!asConversation}
|
||||
email={full ?? m}
|
||||
expanded={open}
|
||||
replyable={!!full && isReplyableMember(m, scheduledIds)}
|
||||
replyActions={cardReplyActions(m, !!full, scheduledIds)}
|
||||
deferBody={!full || !bodyEnabled}
|
||||
onBodySettled={onBodySettled}
|
||||
onToggleExpanded={() => toggleCard(m.id)}
|
||||
|
||||
Reference in New Issue
Block a user