mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 00:07:01 +00:00
fix: translate the no-subject fallback in mail lists and notifications
The mail list, the unified inbox, a contact's recent emails and new-mail notifications showed a hard-coded English "(no subject)". They now use the webmail's `email_viewer.no_subject` (and `contacts.activity.no_subject` for the contact view). The notification task reads the in-app language override itself, as it cannot use the locale store.
This commit is contained in:
@@ -9,6 +9,7 @@ import type { RootStackParamList } from '../../navigation/types';
|
||||
import { getEmails, queryEmailsByFilter } from '../../api/email';
|
||||
import { getEvents, queryEvents } from '../../api/calendar';
|
||||
import { useCalendarStore } from '../../stores/calendar-store';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { getEventColor } from '../../lib/calendar-utils';
|
||||
import { hasCalendarCapability } from '../../lib/capabilities';
|
||||
import SenderAvatar from '../SenderAvatar';
|
||||
@@ -90,6 +91,7 @@ export function ContactActivity({ contact }: Props) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const navigation = useNavigation<Nav>();
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const calendarEvents = useCalendarStore((s) => s.events);
|
||||
const calendars = useCalendarStore((s) => s.calendars);
|
||||
|
||||
@@ -219,7 +221,7 @@ export function ContactActivity({ contact }: Props) {
|
||||
<Text style={styles.rowDate}>{formatRelativeDate(email.receivedAt)}</Text>
|
||||
</View>
|
||||
<Text style={styles.rowSubject} numberOfLines={1}>
|
||||
{email.subject || '(no subject)'}
|
||||
{email.subject || t('contacts.activity.no_subject', '(No subject)')}
|
||||
</Text>
|
||||
{email.preview ? (
|
||||
<Text style={styles.rowPreview} numberOfLines={1}>{email.preview}</Text>
|
||||
|
||||
@@ -155,6 +155,35 @@ describe('pushBackgroundTask notifications', () => {
|
||||
expect.objectContaining({ emailId: 'm1', accountId: LOCAL, jmapAccountId: 'jmap-primary' }),
|
||||
);
|
||||
});
|
||||
|
||||
it("says a message has no subject in the app's language", async () => {
|
||||
await AsyncStorage.setItem('webmail:locale:v1', JSON.stringify({ override: 'de' }));
|
||||
(secureFetch as ReturnType<typeof vi.fn>).mockImplementation(async (url: string) => ({
|
||||
ok: true,
|
||||
json: async () => (url.endsWith('/.well-known/jmap')
|
||||
? {
|
||||
apiUrl: 'https://mail.example.com/jmap/',
|
||||
primaryAccounts: { 'urn:ietf:params:jmap:mail': 'jmap-primary' },
|
||||
accounts: { 'jmap-primary': {} },
|
||||
}
|
||||
: {
|
||||
methodResponses: [[
|
||||
'Email/get',
|
||||
{ list: [{ id: 'm1', threadId: 't1', keywords: {}, subject: '', from: [{ email: 'bob@example.com' }] }] },
|
||||
'0',
|
||||
]],
|
||||
}),
|
||||
}));
|
||||
|
||||
await pushBackgroundTask({
|
||||
kind: 'jmap-email-push',
|
||||
accountLabel: 'alice',
|
||||
accountId: 'jmap-primary',
|
||||
emailIds: JSON.stringify(['m1']),
|
||||
});
|
||||
|
||||
expect(showNotification).toHaveBeenCalledWith(expect.objectContaining({ emailId: 'm1', body: '(Kein Betreff)' }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectNotifiableEmails', () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { NativeModules } from 'react-native';
|
||||
import { jmapClient, type StoredCredentials } from '../api/jmap-client';
|
||||
import { CAPABILITIES } from '../api/types';
|
||||
import type { Email, JMAPMethodCall, JMAPSession, Mailbox } from '../api/types';
|
||||
import { detectDeviceLocale, isSupportedLocale, translate, type LocaleCode } from '../i18n';
|
||||
import { secureFetch } from './client-cert';
|
||||
import { refreshOAuthAccessToken, type OAuthTokens } from './oauth';
|
||||
import {
|
||||
@@ -23,6 +24,8 @@ import {
|
||||
// import the Zustand store (would pull in React) so we read AsyncStorage
|
||||
// directly. Keep this in sync if the store key ever changes.
|
||||
const SETTINGS_STORAGE_KEY = 'webmail:settings:v1';
|
||||
// Mirrors STORAGE_KEY in `stores/locale-store.ts`, for the same reason.
|
||||
const LOCALE_STORAGE_KEY = 'webmail:locale:v1';
|
||||
// Mirrors the persist name in `stores/account-store.ts`. Used to map the
|
||||
// relay's `accountLabel` (= username) back to a local account when the JMAP
|
||||
// account id is not known yet.
|
||||
@@ -70,6 +73,16 @@ export async function senderFaviconsAllowed(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/** The app's language: the in-app override, else the device's. */
|
||||
export async function notificationLocale(): Promise<LocaleCode> {
|
||||
try {
|
||||
const raw = await AsyncStorage.getItem(LOCALE_STORAGE_KEY);
|
||||
const override = raw ? (JSON.parse(raw) as { override?: unknown }).override : null;
|
||||
if (typeof override === 'string' && isSupportedLocale(override)) return override;
|
||||
} catch { /* the device language it is */ }
|
||||
return detectDeviceLocale();
|
||||
}
|
||||
|
||||
interface ShowNotificationOptions {
|
||||
notificationId: string;
|
||||
title: string;
|
||||
@@ -449,6 +462,7 @@ async function processAccountForPush(accountId: string, payload: RelayPushData):
|
||||
const groupKey = `bulwark-mail:${accountId}`;
|
||||
const groupTitle = payload.accountLabel ?? accountId.split('@')[0] ?? accountId;
|
||||
const favicons = await senderFaviconsAllowed();
|
||||
const locale = await notificationLocale();
|
||||
|
||||
// Oldest first so the newest ends up on top of the tray.
|
||||
const ordered = [...toNotify].sort(
|
||||
@@ -459,7 +473,7 @@ async function processAccountForPush(accountId: string, payload: RelayPushData):
|
||||
const name = from?.name ?? '';
|
||||
const address = from?.email ?? '';
|
||||
const title = name || address || 'New mail';
|
||||
const body = email.subject || '(no subject)';
|
||||
const body = email.subject || translate(locale, 'email_viewer.no_subject', '(No Subject)');
|
||||
const initials = getEmailInitials(name, address);
|
||||
const bgColorHex = hslToHex(generateEmailAvatarColor(name, address));
|
||||
const faviconDomain = favicons ? getFaviconDomain(address) : null;
|
||||
|
||||
@@ -219,7 +219,7 @@ const EmailRow = React.memo(function EmailRow({
|
||||
{/* Row 2: Subject + tag pills */}
|
||||
<View style={styles.subjectRow}>
|
||||
<Text style={[styles.emailSubject, dyn.body, unread && styles.textBold]} numberOfLines={1}>
|
||||
{singleLine(item.subject) || '(no subject)'}
|
||||
{singleLine(item.subject) || tr('email_viewer.no_subject', '(No Subject)')}
|
||||
</Text>
|
||||
{tags.slice(0, 3).map((tag) => (
|
||||
<View key={tag.id} style={[styles.tagPill, { backgroundColor: tag.bg }]}>
|
||||
|
||||
@@ -320,7 +320,7 @@ export default function UnifiedInboxScreen({ navigation, route }: Props) {
|
||||
<Text style={styles.time}>{formatListDate(item.receivedAt, { dateFormat, timeFormat, locale, t })}</Text>
|
||||
</View>
|
||||
<Text style={[styles.subject, unread && styles.bold]} numberOfLines={1}>
|
||||
{singleLine(item.subject) || '(no subject)'}
|
||||
{singleLine(item.subject) || t('email_viewer.no_subject', '(No Subject)')}
|
||||
</Text>
|
||||
<View style={styles.line}>
|
||||
{acc && accountIds.length > 1 && (
|
||||
|
||||
Reference in New Issue
Block a user