mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 08:15:55 +00:00
feat: jump to the next unread message with n #514
This commit is contained in:
@@ -53,6 +53,7 @@ import { useDeviceDetection } from "@/hooks/use-media-query";
|
||||
import { usePaneSize } from "@/hooks/use-pane-size";
|
||||
import { usePaneId } from "@/hooks/use-pane-context";
|
||||
import { useKeyboardShortcuts, type GoToMailboxTarget } from "@/hooks/use-keyboard-shortcuts";
|
||||
import { findNextUnreadIndex } from "@/lib/next-unread-email";
|
||||
import { useRefreshGesture } from "@/hooks/use-refresh-gesture";
|
||||
import { useConfirmDialog } from "@/hooks/use-confirm-dialog";
|
||||
import { usePromptDialog } from "@/hooks/use-prompt-dialog";
|
||||
@@ -849,6 +850,17 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
|
||||
if (mvShell) setCursorOnly(true);
|
||||
}
|
||||
},
|
||||
// `n`: open the next unread message below the current one, in the order
|
||||
// the list has loaded. Only the loaded rows count; say so when none is
|
||||
// left instead of wrapping to the top. (#514)
|
||||
onNextUnread: () => {
|
||||
const nextIndex = findNextUnreadIndex(activeEmails, selectedEmail?.id);
|
||||
if (nextIndex === -1) {
|
||||
toast.info(t('shortcuts.navigation.no_more_unread'));
|
||||
return;
|
||||
}
|
||||
void handleEmailSelect(activeEmails[nextIndex]);
|
||||
},
|
||||
onOpenEmail: () => {
|
||||
// Outside Mountain View the email is already open by virtue of being
|
||||
// selected; under it this is the step that promotes cursor to open.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { useKeyboardShortcuts } from '../use-keyboard-shortcuts';
|
||||
import { useKeyboardShortcuts, KEYBOARD_SHORTCUTS } from '../use-keyboard-shortcuts';
|
||||
|
||||
// #683: the help modal advertises `x` for thread expansion and the hook binds
|
||||
// it; the handler must actually be invoked when a message is selected.
|
||||
@@ -90,3 +90,43 @@ describe('useKeyboardShortcuts: Shift + arrow extends the selection', () => {
|
||||
expect(onExtendSelection).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
// #514: `n` jumps to the next unread message, with or without one open.
|
||||
describe('useKeyboardShortcuts: n opens the next unread message', () => {
|
||||
function pressN(shiftKey = false) {
|
||||
document.dispatchEvent(new KeyboardEvent('keydown', { key: shiftKey ? 'N' : 'n', code: 'KeyN', shiftKey, bubbles: true }));
|
||||
}
|
||||
|
||||
it('calls onNextUnread with a message open', () => {
|
||||
const onNextUnread = vi.fn();
|
||||
renderHook(() =>
|
||||
useKeyboardShortcuts({ enabled: true, emails: [], selectedEmailId: 'e1', handlers: { onNextUnread } }),
|
||||
);
|
||||
|
||||
pressN();
|
||||
|
||||
expect(onNextUnread).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onNextUnread with nothing open', () => {
|
||||
const onNextUnread = vi.fn();
|
||||
renderHook(() => useKeyboardShortcuts({ enabled: true, emails: [], handlers: { onNextUnread } }));
|
||||
|
||||
pressN();
|
||||
|
||||
expect(onNextUnread).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('ignores Shift + N', () => {
|
||||
const onNextUnread = vi.fn();
|
||||
renderHook(() => useKeyboardShortcuts({ enabled: true, emails: [], handlers: { onNextUnread } }));
|
||||
|
||||
pressN(true);
|
||||
|
||||
expect(onNextUnread).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('is listed in the help modal under navigation', () => {
|
||||
expect(KEYBOARD_SHORTCUTS.navigation).toContainEqual({ key: 'n', description: 'shortcuts.navigation.next_unread' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,8 @@ export interface KeyboardShortcutHandlers {
|
||||
onPreviousEmail?: () => void;
|
||||
onOpenEmail?: () => void;
|
||||
onCloseEmail?: () => void;
|
||||
/** `n`: open the next unread message below the current one (#514). */
|
||||
onNextUnread?: () => void;
|
||||
|
||||
// Email actions
|
||||
onReply?: () => void;
|
||||
@@ -319,6 +321,14 @@ export function useKeyboardShortcuts({
|
||||
run("toggle-thread-expansion", h.onToggleThreadExpansion);
|
||||
}
|
||||
break;
|
||||
|
||||
// Navigation: next unread, as in Thunderbird (#514)
|
||||
case "n":
|
||||
if (!event.shiftKey) {
|
||||
event.preventDefault();
|
||||
run("next-unread", h.onNextUnread);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
[selectedEmailId, selectionCount, mountainViewKeys]
|
||||
@@ -387,6 +397,7 @@ export const KEYBOARD_SHORTCUTS = {
|
||||
{ key: "k / ↑", description: "shortcuts.navigation.previous_email" },
|
||||
{ key: "Enter / o", description: "shortcuts.navigation.open_email" },
|
||||
{ key: "Esc", description: "shortcuts.navigation.close_email" },
|
||||
{ key: "n", description: "shortcuts.navigation.next_unread" },
|
||||
{ key: "g → i / s / t / d / a", description: "shortcuts.navigation.go_to_folder", layout: "mountain-view" },
|
||||
],
|
||||
actions: [
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { findNextUnreadIndex } from '../next-unread-email';
|
||||
|
||||
// #514: `n` opens the next unread message below the current one.
|
||||
|
||||
const mail = (id: string, seen: boolean): { id: string; keywords: Record<string, boolean> } =>
|
||||
({ id, keywords: seen ? { $seen: true } : {} });
|
||||
|
||||
describe('findNextUnreadIndex', () => {
|
||||
const list = [mail('a', false), mail('b', true), mail('c', true), mail('d', false), mail('e', true)];
|
||||
|
||||
it('finds the first unread message after the current one', () => {
|
||||
expect(findNextUnreadIndex(list, 'b')).toBe(3);
|
||||
});
|
||||
|
||||
it('skips the current message even when it is unread', () => {
|
||||
expect(findNextUnreadIndex(list, 'a')).toBe(3);
|
||||
});
|
||||
|
||||
it('starts at the top when nothing is open', () => {
|
||||
expect(findNextUnreadIndex(list, undefined)).toBe(0);
|
||||
expect(findNextUnreadIndex(list, null)).toBe(0);
|
||||
});
|
||||
|
||||
it('starts at the top when the open message is not in the list', () => {
|
||||
expect(findNextUnreadIndex(list, 'gone')).toBe(0);
|
||||
});
|
||||
|
||||
it('does not wrap around to unread mail above', () => {
|
||||
expect(findNextUnreadIndex(list, 'd')).toBe(-1);
|
||||
expect(findNextUnreadIndex(list, 'e')).toBe(-1);
|
||||
});
|
||||
|
||||
it('treats a message without keywords as unread', () => {
|
||||
const rows = [mail('a', true), { id: 'b' } as { id: string; keywords: Record<string, boolean> }];
|
||||
expect(findNextUnreadIndex(rows, 'a')).toBe(1);
|
||||
});
|
||||
|
||||
it('returns -1 for an empty or all-read list', () => {
|
||||
expect(findNextUnreadIndex([], undefined)).toBe(-1);
|
||||
expect(findNextUnreadIndex([mail('a', true), mail('b', true)], undefined)).toBe(-1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Email } from "@/lib/jmap/types";
|
||||
|
||||
/**
|
||||
* Index of the first unread message after `currentId` in the loaded list, or
|
||||
* -1 when there is none below it. With nothing open (or the open message no
|
||||
* longer in the list) the search starts at the top. It does not wrap around:
|
||||
* unread mail above the current one is left for the user to scroll back to.
|
||||
* (#514)
|
||||
*/
|
||||
export function findNextUnreadIndex(
|
||||
emails: readonly Pick<Email, "id" | "keywords">[],
|
||||
currentId: string | null | undefined,
|
||||
): number {
|
||||
const currentIndex = currentId ? emails.findIndex((e) => e.id === currentId) : -1;
|
||||
for (let i = currentIndex + 1; i < emails.length; i++) {
|
||||
if (!emails[i].keywords?.$seen) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "الرسالة السابقة",
|
||||
"open_email": "فتح الرسالة",
|
||||
"close_email": "إغلاق / إلغاء التحديد",
|
||||
"go_to_folder": "الانتقال إلى المجلد"
|
||||
"go_to_folder": "الانتقال إلى المجلد",
|
||||
"next_unread": "الرسالة غير المقروءة التالية",
|
||||
"no_more_unread": "لا توجد رسائل غير مقروءة أسفل القائمة"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "رد",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Correu anterior",
|
||||
"open_email": "Obre el correu",
|
||||
"close_email": "Tanca / Desselecciona",
|
||||
"go_to_folder": "Vés a la carpeta"
|
||||
"go_to_folder": "Vés a la carpeta",
|
||||
"next_unread": "Següent correu no llegit",
|
||||
"no_more_unread": "No hi ha més correus no llegits més avall a la llista"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Respon",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Předchozí zpráva",
|
||||
"open_email": "Otevřít zprávu",
|
||||
"close_email": "Zavřít / Zrušit výběr",
|
||||
"go_to_folder": "Přejít do složky"
|
||||
"go_to_folder": "Přejít do složky",
|
||||
"next_unread": "Další nepřečtený e-mail",
|
||||
"no_more_unread": "Níže v seznamu už nejsou žádné nepřečtené e-maily"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Odpovědět",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Forrige e-mail",
|
||||
"open_email": "Åbn e-mail",
|
||||
"close_email": "Luk / Fravaælg",
|
||||
"go_to_folder": "Gå til mappe"
|
||||
"go_to_folder": "Gå til mappe",
|
||||
"next_unread": "Næste ulæste e-mail",
|
||||
"no_more_unread": "Der er ikke flere ulæste e-mails længere nede på listen"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Svar",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Vorherige E-Mail",
|
||||
"open_email": "E-Mail öffnen",
|
||||
"close_email": "Schließen / Auswahl aufheben",
|
||||
"go_to_folder": "Zum Ordner springen"
|
||||
"go_to_folder": "Zum Ordner springen",
|
||||
"next_unread": "Nächste ungelesene E-Mail",
|
||||
"no_more_unread": "Weiter unten in der Liste gibt es keine ungelesenen E-Mails"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Antworten",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Previous email",
|
||||
"open_email": "Open email",
|
||||
"close_email": "Close / Deselect",
|
||||
"go_to_folder": "Go to folder"
|
||||
"go_to_folder": "Go to folder",
|
||||
"next_unread": "Next unread email",
|
||||
"no_more_unread": "No unread email further down the list"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Reply",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Correo anterior",
|
||||
"open_email": "Abrir correo",
|
||||
"close_email": "Cerrar / Deseleccionar",
|
||||
"go_to_folder": "Ir a la carpeta"
|
||||
"go_to_folder": "Ir a la carpeta",
|
||||
"next_unread": "Siguiente correo no leído",
|
||||
"no_more_unread": "No hay más correos no leídos más abajo en la lista"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Responder",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "ایمیل قبلی",
|
||||
"open_email": "باز کردن ایمیل",
|
||||
"close_email": "بستن / لغو انتخاب",
|
||||
"go_to_folder": "رفتن به پوشه"
|
||||
"go_to_folder": "رفتن به پوشه",
|
||||
"next_unread": "ایمیل خواندهنشده بعدی",
|
||||
"no_more_unread": "ایمیل خواندهنشدهای پایینتر در فهرست نیست"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "پاسخ",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Email précédent",
|
||||
"open_email": "Ouvrir l'email",
|
||||
"close_email": "Fermer / Désélectionner",
|
||||
"go_to_folder": "Aller au dossier"
|
||||
"go_to_folder": "Aller au dossier",
|
||||
"next_unread": "E-mail non lu suivant",
|
||||
"no_more_unread": "Aucun e-mail non lu plus bas dans la liste"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Répondre",
|
||||
|
||||
@@ -2533,7 +2533,9 @@
|
||||
"previous_email": "אימייל קודם",
|
||||
"open_email": "פתח אימייל",
|
||||
"close_email": "סגור / בטל בחירה",
|
||||
"go_to_folder": "מעבר לתיקייה"
|
||||
"go_to_folder": "מעבר לתיקייה",
|
||||
"next_unread": "ההודעה הבאה שלא נקראה",
|
||||
"no_more_unread": "אין הודעות שלא נקראו בהמשך הרשימה"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "השב",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Előző e-mail",
|
||||
"open_email": "E-mail megnyitása",
|
||||
"close_email": "Bezárás / Kijelölés megszüntetése",
|
||||
"go_to_folder": "Ugrás mappára"
|
||||
"go_to_folder": "Ugrás mappára",
|
||||
"next_unread": "Következő olvasatlan e-mail",
|
||||
"no_more_unread": "Lejjebb a listában nincs több olvasatlan e-mail"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Válasz",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Messaggio precedente",
|
||||
"open_email": "Apri messaggio",
|
||||
"close_email": "Chiudi / Deseleziona",
|
||||
"go_to_folder": "Vai alla cartella"
|
||||
"go_to_folder": "Vai alla cartella",
|
||||
"next_unread": "Prossima email non letta",
|
||||
"no_more_unread": "Nessuna email non letta più in basso nell'elenco"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Rispondi",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "前のメール",
|
||||
"open_email": "メールを開く",
|
||||
"close_email": "閉じる / 選択解除",
|
||||
"go_to_folder": "フォルダーに移動"
|
||||
"go_to_folder": "フォルダーに移動",
|
||||
"next_unread": "次の未読メール",
|
||||
"no_more_unread": "リストのこれより下に未読メールはありません"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "返信",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "이전 메일",
|
||||
"open_email": "메일 열기",
|
||||
"close_email": "닫기 / 선택 해제",
|
||||
"go_to_folder": "폴더로 이동"
|
||||
"go_to_folder": "폴더로 이동",
|
||||
"next_unread": "다음 읽지 않은 메일",
|
||||
"no_more_unread": "목록 아래쪽에 읽지 않은 메일이 더 없습니다"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "답장",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Iepriekšējā vēstule",
|
||||
"open_email": "Atvērt vēstuli",
|
||||
"close_email": "Aizvērt / Noņemt atlasi",
|
||||
"go_to_folder": "Pāriet uz mapi"
|
||||
"go_to_folder": "Pāriet uz mapi",
|
||||
"next_unread": "Nākamais nelasītais e-pasts",
|
||||
"no_more_unread": "Sarakstā zemāk vairs nav nelasītu e-pastu"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Atbildēt",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Өмнөх имэйл",
|
||||
"open_email": "Имэйл нээх",
|
||||
"close_email": "Хаах / Сонголтыг цуцлах",
|
||||
"go_to_folder": "Хавтас руу очих"
|
||||
"go_to_folder": "Хавтас руу очих",
|
||||
"next_unread": "Дараагийн уншаагүй имэйл",
|
||||
"no_more_unread": "Жагсаалтын доод хэсэгт уншаагүй имэйл алга"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Хариулах",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Forrige e-post",
|
||||
"open_email": "Åpne e-post",
|
||||
"close_email": "Lukk / fjern valget",
|
||||
"go_to_folder": "Gå til mappe"
|
||||
"go_to_folder": "Gå til mappe",
|
||||
"next_unread": "Neste uleste e-post",
|
||||
"no_more_unread": "Det er ingen flere uleste e-poster lenger ned i listen"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Svar",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Vorige e-mail",
|
||||
"open_email": "E-mail openen",
|
||||
"close_email": "Sluiten / Deselecteren",
|
||||
"go_to_folder": "Ga naar map"
|
||||
"go_to_folder": "Ga naar map",
|
||||
"next_unread": "Volgende ongelezen e-mail",
|
||||
"no_more_unread": "Verderop in de lijst staan geen ongelezen e-mails meer"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Beantwoorden",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Poprzednia wiadomość",
|
||||
"open_email": "Otwórz wiadomość",
|
||||
"close_email": "Zamknij / Odznacz",
|
||||
"go_to_folder": "Przejdź do folderu"
|
||||
"go_to_folder": "Przejdź do folderu",
|
||||
"next_unread": "Następna nieprzeczytana wiadomość",
|
||||
"no_more_unread": "Niżej na liście nie ma już nieprzeczytanych wiadomości"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Odpowiedz",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "E-mail anterior",
|
||||
"open_email": "Abrir e-mail",
|
||||
"close_email": "Fechar / Desselecionar",
|
||||
"go_to_folder": "Ir para a pasta"
|
||||
"go_to_folder": "Ir para a pasta",
|
||||
"next_unread": "Próximo e-mail não lido",
|
||||
"no_more_unread": "Não há mais e-mails não lidos mais abaixo na lista"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Responder",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "E-mailul anterior",
|
||||
"open_email": "Deschide e-mailul",
|
||||
"close_email": "Închide / Deselectează",
|
||||
"go_to_folder": "Mergi la dosar"
|
||||
"go_to_folder": "Mergi la dosar",
|
||||
"next_unread": "Următorul e-mail necitit",
|
||||
"no_more_unread": "Nu mai sunt e-mailuri necitite mai jos în listă"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Răspunde",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Предыдущее письмо",
|
||||
"open_email": "Открыть письмо",
|
||||
"close_email": "Закрыть / Отменить выбор",
|
||||
"go_to_folder": "Перейти к папке"
|
||||
"go_to_folder": "Перейти к папке",
|
||||
"next_unread": "Следующее непрочитанное письмо",
|
||||
"no_more_unread": "Ниже в списке больше нет непрочитанных писем"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Ответить",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Predchádzajúci e-mail",
|
||||
"open_email": "Otvoriť e-mail",
|
||||
"close_email": "Zavrieť / Zrušiť výber",
|
||||
"go_to_folder": "Prejsť do priečinka"
|
||||
"go_to_folder": "Prejsť do priečinka",
|
||||
"next_unread": "Ďalší neprečítaný e-mail",
|
||||
"no_more_unread": "Nižšie v zozname už nie sú žiadne neprečítané e-maily"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Odpovedať",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Önceki e-posta",
|
||||
"open_email": "E-postayı aç",
|
||||
"close_email": "Kapat / Seçimi kaldır",
|
||||
"go_to_folder": "Klasöre git"
|
||||
"go_to_folder": "Klasöre git",
|
||||
"next_unread": "Sonraki okunmamış e-posta",
|
||||
"no_more_unread": "Listenin aşağısında okunmamış e-posta yok"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Yanıtla",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "Попередній електронний лист",
|
||||
"open_email": "Відкрийте електронну пошту",
|
||||
"close_email": "Закрити / Скасувати вибір",
|
||||
"go_to_folder": "Перейти до теки"
|
||||
"go_to_folder": "Перейти до теки",
|
||||
"next_unread": "Наступний непрочитаний лист",
|
||||
"no_more_unread": "Нижче в списку більше немає непрочитаних листів"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "Відповісти",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "上一封郵件",
|
||||
"open_email": "開啟郵件",
|
||||
"close_email": "關閉/取消選取",
|
||||
"go_to_folder": "跳至資料夾"
|
||||
"go_to_folder": "跳至資料夾",
|
||||
"next_unread": "下一封未讀郵件",
|
||||
"no_more_unread": "清單下方沒有更多未讀郵件"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "回覆",
|
||||
|
||||
@@ -2615,7 +2615,9 @@
|
||||
"previous_email": "上一封邮件",
|
||||
"open_email": "打开邮件",
|
||||
"close_email": "关闭/取消选择",
|
||||
"go_to_folder": "跳转到文件夹"
|
||||
"go_to_folder": "跳转到文件夹",
|
||||
"next_unread": "下一封未读邮件",
|
||||
"no_more_unread": "列表下方没有更多未读邮件"
|
||||
},
|
||||
"actions": {
|
||||
"reply": "回复",
|
||||
|
||||
Reference in New Issue
Block a user