Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c4f6a9f8e | ||
|
|
2a323d6270 | ||
|
|
d282813bc5 | ||
|
|
d599e7404f |
Generated
+17
-1
@@ -14,7 +14,8 @@
|
|||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"typescript": "^7.0.2"
|
"typescript": "^7.0.2",
|
||||||
|
"typescript-ast": "npm:typescript@^5.9.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19"
|
"node": ">=20.19"
|
||||||
@@ -2777,6 +2778,21 @@
|
|||||||
"@typescript/typescript-win32-x64": "7.0.2"
|
"@typescript/typescript-win32-x64": "7.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typescript-ast": {
|
||||||
|
"name": "typescript",
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "8.9.0",
|
"version": "8.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
|
||||||
|
|||||||
+2
-1
@@ -28,6 +28,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"typescript": "^7.0.2"
|
"typescript": "^7.0.2",
|
||||||
|
"typescript-ast": "npm:typescript@^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,21 @@
|
|||||||
* in the file looking correct, is never looked up, and the app renders English
|
* in the file looking correct, is never looked up, and the app renders English
|
||||||
* for ever. Nothing warns, because a catalogue is only ever read by key.
|
* for ever. Nothing warns, because a catalogue is only ever read by key.
|
||||||
*/
|
*/
|
||||||
import ts from "typescript";
|
/*
|
||||||
|
* The parser, not the compiler.
|
||||||
|
*
|
||||||
|
* TypeScript 7 is the native port: its package ships a `tsc` shim over a Go
|
||||||
|
* binary and nothing else, so `typescript` now exports `version` and
|
||||||
|
* `versionMajorMinor` and no compiler API at all. Every `ts.createSourceFile`
|
||||||
|
* in this directory started throwing "Cannot read properties of undefined
|
||||||
|
* (reading 'Latest')" the day the bump landed, and nothing noticed, because no
|
||||||
|
* workflow runs these.
|
||||||
|
*
|
||||||
|
* `typescript-ast` is an npm alias for the last TypeScript that carries the JS
|
||||||
|
* API (see package.json). It parses; `typescript` still type-checks and builds.
|
||||||
|
* Two entries, two jobs -- not a version someone forgot to remove.
|
||||||
|
*/
|
||||||
|
import ts from "typescript-ast";
|
||||||
import { readFileSync, globSync } from "node:fs";
|
import { readFileSync, globSync } from "node:fs";
|
||||||
|
|
||||||
const wanted = new Set();
|
const wanted = new Set();
|
||||||
|
|||||||
@@ -11,7 +11,21 @@
|
|||||||
* exits non-zero only with --check, so CI can be told to fail on regressions
|
* exits non-zero only with --check, so CI can be told to fail on regressions
|
||||||
* later, once the number is low enough for that to mean something.
|
* later, once the number is low enough for that to mean something.
|
||||||
*/
|
*/
|
||||||
import ts from "typescript";
|
/*
|
||||||
|
* The parser, not the compiler.
|
||||||
|
*
|
||||||
|
* TypeScript 7 is the native port: its package ships a `tsc` shim over a Go
|
||||||
|
* binary and nothing else, so `typescript` now exports `version` and
|
||||||
|
* `versionMajorMinor` and no compiler API at all. Every `ts.createSourceFile`
|
||||||
|
* in this directory started throwing "Cannot read properties of undefined
|
||||||
|
* (reading 'Latest')" the day the bump landed, and nothing noticed, because no
|
||||||
|
* workflow runs these.
|
||||||
|
*
|
||||||
|
* `typescript-ast` is an npm alias for the last TypeScript that carries the JS
|
||||||
|
* API (see package.json). It parses; `typescript` still type-checks and builds.
|
||||||
|
* Two entries, two jobs -- not a version someone forgot to remove.
|
||||||
|
*/
|
||||||
|
import ts from "typescript-ast";
|
||||||
import { readFileSync, globSync } from "node:fs";
|
import { readFileSync, globSync } from "node:fs";
|
||||||
|
|
||||||
/** Attributes a person reads. `className` and `key` are not among them. */
|
/** Attributes a person reads. `className` and `key` are not among them. */
|
||||||
|
|||||||
@@ -12,7 +12,21 @@
|
|||||||
* node scripts/i18n-extract.mjs <file...> rewrite in place
|
* node scripts/i18n-extract.mjs <file...> rewrite in place
|
||||||
* node scripts/i18n-extract.mjs --dry <file...>
|
* node scripts/i18n-extract.mjs --dry <file...>
|
||||||
*/
|
*/
|
||||||
import ts from "typescript";
|
/*
|
||||||
|
* The parser, not the compiler.
|
||||||
|
*
|
||||||
|
* TypeScript 7 is the native port: its package ships a `tsc` shim over a Go
|
||||||
|
* binary and nothing else, so `typescript` now exports `version` and
|
||||||
|
* `versionMajorMinor` and no compiler API at all. Every `ts.createSourceFile`
|
||||||
|
* in this directory started throwing "Cannot read properties of undefined
|
||||||
|
* (reading 'Latest')" the day the bump landed, and nothing noticed, because no
|
||||||
|
* workflow runs these.
|
||||||
|
*
|
||||||
|
* `typescript-ast` is an npm alias for the last TypeScript that carries the JS
|
||||||
|
* API (see package.json). It parses; `typescript` still type-checks and builds.
|
||||||
|
* Two entries, two jobs -- not a version someone forgot to remove.
|
||||||
|
*/
|
||||||
|
import ts from "typescript-ast";
|
||||||
import { readFileSync, writeFileSync } from "node:fs";
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
|
|
||||||
const ATTRS = new Set(["title", "aria-label", "placeholder", "alt", "label", "hint", "confirmLabel", "description"]);
|
const ATTRS = new Set(["title", "aria-label", "placeholder", "alt", "label", "hint", "confirmLabel", "description"]);
|
||||||
|
|||||||
@@ -18,7 +18,21 @@
|
|||||||
* string that is neither -- one no catalogue has a key for, which therefore
|
* string that is neither -- one no catalogue has a key for, which therefore
|
||||||
* cannot be translated at all, however many languages ship.
|
* cannot be translated at all, however many languages ship.
|
||||||
*/
|
*/
|
||||||
import ts from "typescript";
|
/*
|
||||||
|
* The parser, not the compiler.
|
||||||
|
*
|
||||||
|
* TypeScript 7 is the native port: its package ships a `tsc` shim over a Go
|
||||||
|
* binary and nothing else, so `typescript` now exports `version` and
|
||||||
|
* `versionMajorMinor` and no compiler API at all. Every `ts.createSourceFile`
|
||||||
|
* in this directory started throwing "Cannot read properties of undefined
|
||||||
|
* (reading 'Latest')" the day the bump landed, and nothing noticed, because no
|
||||||
|
* workflow runs these.
|
||||||
|
*
|
||||||
|
* `typescript-ast` is an npm alias for the last TypeScript that carries the JS
|
||||||
|
* API (see package.json). It parses; `typescript` still type-checks and builds.
|
||||||
|
* Two entries, two jobs -- not a version someone forgot to remove.
|
||||||
|
*/
|
||||||
|
import ts from "typescript-ast";
|
||||||
import { readFileSync, globSync } from "node:fs";
|
import { readFileSync, globSync } from "node:fs";
|
||||||
|
|
||||||
/* Where a string literal in this position is shown to somebody. */
|
/* Where a string literal in this position is shown to somebody. */
|
||||||
|
|||||||
@@ -9,7 +9,21 @@
|
|||||||
* exists is dead weight, but a call with no key is an untranslated string
|
* exists is dead weight, but a call with no key is an untranslated string
|
||||||
* nobody noticed.
|
* nobody noticed.
|
||||||
*/
|
*/
|
||||||
import ts from "typescript";
|
/*
|
||||||
|
* The parser, not the compiler.
|
||||||
|
*
|
||||||
|
* TypeScript 7 is the native port: its package ships a `tsc` shim over a Go
|
||||||
|
* binary and nothing else, so `typescript` now exports `version` and
|
||||||
|
* `versionMajorMinor` and no compiler API at all. Every `ts.createSourceFile`
|
||||||
|
* in this directory started throwing "Cannot read properties of undefined
|
||||||
|
* (reading 'Latest')" the day the bump landed, and nothing noticed, because no
|
||||||
|
* workflow runs these.
|
||||||
|
*
|
||||||
|
* `typescript-ast` is an npm alias for the last TypeScript that carries the JS
|
||||||
|
* API (see package.json). It parses; `typescript` still type-checks and builds.
|
||||||
|
* Two entries, two jobs -- not a version someone forgot to remove.
|
||||||
|
*/
|
||||||
|
import ts from "typescript-ast";
|
||||||
import { readFileSync, globSync } from "node:fs";
|
import { readFileSync, globSync } from "node:fs";
|
||||||
|
|
||||||
const strings = new Set();
|
const strings = new Set();
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { rowIsOpen, visibleMessages } from "../openMessage";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reported from the inbox: with conversation view off, the list showed the two
|
||||||
|
* messages of a thread as separate rows -- correctly -- but clicking either one
|
||||||
|
* highlighted *both* and filled the reading pane with all five messages of the
|
||||||
|
* conversation.
|
||||||
|
*
|
||||||
|
* The setting reached only as far as `collapseThreads` on the query. These are
|
||||||
|
* the two rules that were missing downstream.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const msg = (id: string) => ({ id });
|
||||||
|
|
||||||
|
describe("which row is drawn as open", () => {
|
||||||
|
it("marks only the opened message, not its siblings", () => {
|
||||||
|
// The reported case: two rows, one thread, one of them opened.
|
||||||
|
expect(rowIsOpen("m1", "t1", "m1", "t1")).toBe(true);
|
||||||
|
expect(rowIsOpen("m2", "t1", "m1", "t1")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still marks the whole thread when conversation view is on", () => {
|
||||||
|
// No message singled out: every row of the open thread is part of what the
|
||||||
|
// reading pane is showing, so every one of them is open.
|
||||||
|
expect(rowIsOpen("m1", "t1", null, "t1")).toBe(true);
|
||||||
|
expect(rowIsOpen("m2", "t1", null, "t1")).toBe(true);
|
||||||
|
expect(rowIsOpen("m3", "t2", null, "t1")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("marks nothing when nothing is open", () => {
|
||||||
|
expect(rowIsOpen("m1", "t1", null, null)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mark a row whose thread is unknown", () => {
|
||||||
|
// A row whose email has not loaded yet has no thread id; `undefined` must
|
||||||
|
// not match a null openThreadId and light the row up.
|
||||||
|
expect(rowIsOpen("m1", undefined, null, null)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("which messages the reading pane shows", () => {
|
||||||
|
const thread = [msg("a"), msg("b"), msg("c")];
|
||||||
|
|
||||||
|
it("shows just the opened message", () => {
|
||||||
|
expect(visibleMessages(thread, "b")).toEqual([msg("b")]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the whole thread when none is singled out", () => {
|
||||||
|
expect(visibleMessages(thread, null)).toEqual(thread);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the thread when the id names nothing in it", () => {
|
||||||
|
/*
|
||||||
|
* Two ways to arrive here: a link shared by somebody whose conversation
|
||||||
|
* view is on, and an `m` parameter left in the URL when the setting is
|
||||||
|
* switched back. A conversation is a better answer to both than an empty
|
||||||
|
* pane, which is what filtering to nothing would produce.
|
||||||
|
*/
|
||||||
|
expect(visibleMessages(thread, "zzz")).toEqual(thread);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves an empty thread empty rather than inventing a message", () => {
|
||||||
|
expect(visibleMessages([], "b")).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* What "open" means when conversation view is off.
|
||||||
|
*
|
||||||
|
* The setting used to reach only as far as the query -- it set `collapseThreads`
|
||||||
|
* and nothing else -- so the list showed individual messages while everything
|
||||||
|
* downstream still worked in threads. Opening one message highlighted every row
|
||||||
|
* in its thread and filled the reading pane with the whole conversation, which
|
||||||
|
* is exactly the grouping the setting was turned off to avoid.
|
||||||
|
*
|
||||||
|
* Both halves are the same question asked in two places, so they live together.
|
||||||
|
*/
|
||||||
|
import type { Id } from "@/jmap/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a list row should be drawn as the open one.
|
||||||
|
*
|
||||||
|
* With a message singled out the row must match it exactly. Matching on the
|
||||||
|
* thread is what lit up every sibling.
|
||||||
|
*/
|
||||||
|
export function rowIsOpen(rowId: Id, rowThreadId: Id | undefined, openMessageId: Id | null, openThreadId: Id | null): boolean {
|
||||||
|
if (openMessageId) return rowId === openMessageId;
|
||||||
|
return Boolean(openThreadId) && rowThreadId === openThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The messages the reading pane should render.
|
||||||
|
*
|
||||||
|
* Falls back to the whole thread when the id names nothing in it. That is what
|
||||||
|
* a link from somebody with conversation view *on* looks like, and what a
|
||||||
|
* lingering `m` parameter looks like after the setting is switched back -- a
|
||||||
|
* conversation is a better answer to both than an empty pane.
|
||||||
|
*/
|
||||||
|
export function visibleMessages<T extends { id: Id }>(messages: T[], openMessageId: Id | null): T[] {
|
||||||
|
if (!openMessageId) return messages;
|
||||||
|
const single = messages.filter((m) => m.id === openMessageId);
|
||||||
|
return single.length ? single : messages;
|
||||||
|
}
|
||||||
@@ -756,6 +756,7 @@ export const catalog: Catalog = {
|
|||||||
"Open the Mail view to see all shortcuts.": "Öffnen Sie die E-Mail-Ansicht, um alle Tastenkürzel zu sehen.",
|
"Open the Mail view to see all shortcuts.": "Öffnen Sie die E-Mail-Ansicht, um alle Tastenkürzel zu sehen.",
|
||||||
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Tastenkürzel im Gmail-Stil sind immer aktiv. Drücken Sie überall {key}, um diese Liste zu sehen.",
|
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Tastenkürzel im Gmail-Stil sind immer aktiv. Drücken Sie überall {key}, um diese Liste zu sehen.",
|
||||||
"Select a conversation to read it here · Press {key} for shortcuts": "Wählen Sie eine Konversation, um sie hier zu lesen · {key} für Tastenkürzel",
|
"Select a conversation to read it here · Press {key} for shortcuts": "Wählen Sie eine Konversation, um sie hier zu lesen · {key} für Tastenkürzel",
|
||||||
|
"Select a message to read it here · Press {key} for shortcuts": "Wählen Sie eine Nachricht, um sie hier zu lesen · {key} für Tastenkürzel",
|
||||||
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Tipp: Drücken Sie {key} auf einer Konversation, um Labels zu vergeben. Suchen Sie mit {operator}.",
|
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Tipp: Drücken Sie {key} auf einer Konversation, um Labels zu vergeben. Suchen Sie mit {operator}.",
|
||||||
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Eine schnelle, freundliche Open-Source-Webmail für {server}, auf JMAP aufgebaut.",
|
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Eine schnelle, freundliche Open-Source-Webmail für {server}, auf JMAP aufgebaut.",
|
||||||
"Defaults for the calendar views and new events.": "Vorgaben für die Kalenderansichten und neue Termine.",
|
"Defaults for the calendar views and new events.": "Vorgaben für die Kalenderansichten und neue Termine.",
|
||||||
@@ -834,6 +835,7 @@ export const catalog: Catalog = {
|
|||||||
"Nothing": "Nichts",
|
"Nothing": "Nichts",
|
||||||
|
|
||||||
"No conversation selected": "Keine Konversation ausgewählt",
|
"No conversation selected": "Keine Konversation ausgewählt",
|
||||||
|
"No message selected": "Keine Nachricht ausgewählt",
|
||||||
"Drop here for the top level": "Hierher ziehen für die oberste Ebene",
|
"Drop here for the top level": "Hierher ziehen für die oberste Ebene",
|
||||||
|
|
||||||
// ── Remaining prose ────────────────────────────────────────────────
|
// ── Remaining prose ────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -811,6 +811,7 @@ export const catalog: Catalog = {
|
|||||||
"Rename folder": "Cambiar el nombre de la carpeta",
|
"Rename folder": "Cambiar el nombre de la carpeta",
|
||||||
"Search: {query}": "Búsqueda: {query}",
|
"Search: {query}": "Búsqueda: {query}",
|
||||||
"No conversation selected": "Ninguna conversación seleccionada",
|
"No conversation selected": "Ninguna conversación seleccionada",
|
||||||
|
"No message selected": "Ningún mensaje seleccionado",
|
||||||
"Drop here for the top level": "Suelte aquí para el nivel superior",
|
"Drop here for the top level": "Suelte aquí para el nivel superior",
|
||||||
|
|
||||||
// ── Longer prose ───────────────────────────────────────────────────
|
// ── Longer prose ───────────────────────────────────────────────────
|
||||||
@@ -819,6 +820,7 @@ export const catalog: Catalog = {
|
|||||||
"Open the Mail view to see all shortcuts.": "Abra la vista de Correo para ver todos los atajos.",
|
"Open the Mail view to see all shortcuts.": "Abra la vista de Correo para ver todos los atajos.",
|
||||||
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Los atajos al estilo de Gmail están siempre activos. Pulse {key} en cualquier momento para ver esta lista.",
|
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Los atajos al estilo de Gmail están siempre activos. Pulse {key} en cualquier momento para ver esta lista.",
|
||||||
"Select a conversation to read it here · Press {key} for shortcuts": "Seleccione una conversación para leerla aquí · {key} para los atajos",
|
"Select a conversation to read it here · Press {key} for shortcuts": "Seleccione una conversación para leerla aquí · {key} para los atajos",
|
||||||
|
"Select a message to read it here · Press {key} for shortcuts": "Seleccione un mensaje para leerlo aquí · {key} para los atajos",
|
||||||
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Consejo: pulse {key} sobre una conversación para aplicar etiquetas. Busque con {operator}.",
|
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Consejo: pulse {key} sobre una conversación para aplicar etiquetas. Busque con {operator}.",
|
||||||
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Un webmail libre, rápido y agradable para {server}, construido sobre JMAP.",
|
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Un webmail libre, rápido y agradable para {server}, construido sobre JMAP.",
|
||||||
"Defaults for the calendar views and new events.": "Valores predeterminados de las vistas del calendario y de los eventos nuevos.",
|
"Defaults for the calendar views and new events.": "Valores predeterminados de las vistas del calendario y de los eventos nuevos.",
|
||||||
|
|||||||
@@ -816,6 +816,7 @@ export const catalog: Catalog = {
|
|||||||
"Rename folder": "Renommer le dossier",
|
"Rename folder": "Renommer le dossier",
|
||||||
"Search: {query}": "Recherche : {query}",
|
"Search: {query}": "Recherche : {query}",
|
||||||
"No conversation selected": "Aucune conversation sélectionnée",
|
"No conversation selected": "Aucune conversation sélectionnée",
|
||||||
|
"No message selected": "Aucun message sélectionné",
|
||||||
"Drop here for the top level": "Déposer ici pour le niveau supérieur",
|
"Drop here for the top level": "Déposer ici pour le niveau supérieur",
|
||||||
|
|
||||||
// ── Longer prose ───────────────────────────────────────────────────
|
// ── Longer prose ───────────────────────────────────────────────────
|
||||||
@@ -824,6 +825,7 @@ export const catalog: Catalog = {
|
|||||||
"Open the Mail view to see all shortcuts.": "Ouvrez la vue E-mail pour voir tous les raccourcis.",
|
"Open the Mail view to see all shortcuts.": "Ouvrez la vue E-mail pour voir tous les raccourcis.",
|
||||||
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Les raccourcis façon Gmail sont toujours actifs. Appuyez sur {key} n'importe où pour afficher cette liste.",
|
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Les raccourcis façon Gmail sont toujours actifs. Appuyez sur {key} n'importe où pour afficher cette liste.",
|
||||||
"Select a conversation to read it here · Press {key} for shortcuts": "Sélectionnez une conversation pour la lire ici · {key} pour les raccourcis",
|
"Select a conversation to read it here · Press {key} for shortcuts": "Sélectionnez une conversation pour la lire ici · {key} pour les raccourcis",
|
||||||
|
"Select a message to read it here · Press {key} for shortcuts": "Sélectionnez un message pour le lire ici · {key} pour les raccourcis",
|
||||||
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Astuce : appuyez sur {key} sur une conversation pour appliquer des libellés. Recherchez avec {operator}.",
|
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Astuce : appuyez sur {key} sur une conversation pour appliquer des libellés. Recherchez avec {operator}.",
|
||||||
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Un webmail libre, rapide et agréable pour {server}, bâti sur JMAP.",
|
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Un webmail libre, rapide et agréable pour {server}, bâti sur JMAP.",
|
||||||
"Defaults for the calendar views and new events.": "Valeurs par défaut des vues d'agenda et des nouveaux événements.",
|
"Defaults for the calendar views and new events.": "Valeurs par défaut des vues d'agenda et des nouveaux événements.",
|
||||||
|
|||||||
@@ -747,6 +747,7 @@ export const catalog: Catalog = {
|
|||||||
"Open the Mail view to see all shortcuts.": "すべてのショートカットはメール画面で確認できます。",
|
"Open the Mail view to see all shortcuts.": "すべてのショートカットはメール画面で確認できます。",
|
||||||
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Gmail 形式のショートカットは常に有効です。どこでも {key} を押すとこの一覧を表示します。",
|
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Gmail 形式のショートカットは常に有効です。どこでも {key} を押すとこの一覧を表示します。",
|
||||||
"Select a conversation to read it here · Press {key} for shortcuts": "スレッドを選ぶとここに表示されます · {key} でショートカット一覧",
|
"Select a conversation to read it here · Press {key} for shortcuts": "スレッドを選ぶとここに表示されます · {key} でショートカット一覧",
|
||||||
|
"Select a message to read it here · Press {key} for shortcuts": "メールを選ぶとここに表示されます · {key} でショートカット一覧",
|
||||||
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "ヒント: スレッド上で {key} を押すとラベルを付けられます。検索には {operator} が使えます。",
|
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "ヒント: スレッド上で {key} を押すとラベルを付けられます。検索には {operator} が使えます。",
|
||||||
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "{server} のための、軽快で使いやすいオープンソースのウェブメール。JMAP で動作します。",
|
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "{server} のための、軽快で使いやすいオープンソースのウェブメール。JMAP で動作します。",
|
||||||
|
|
||||||
@@ -843,6 +844,7 @@ export const catalog: Catalog = {
|
|||||||
"Not spam": "迷惑メールではない",
|
"Not spam": "迷惑メールではない",
|
||||||
"Nothing": "何もしない",
|
"Nothing": "何もしない",
|
||||||
"No conversation selected": "スレッドが選択されていません",
|
"No conversation selected": "スレッドが選択されていません",
|
||||||
|
"No message selected": "メールが選択されていません",
|
||||||
"Drop here for the top level": "ここにドロップすると最上位へ移動します",
|
"Drop here for the top level": "ここにドロップすると最上位へ移動します",
|
||||||
"Later today": "今日のうちに",
|
"Later today": "今日のうちに",
|
||||||
"Tomorrow morning": "明日の朝",
|
"Tomorrow morning": "明日の朝",
|
||||||
|
|||||||
@@ -807,6 +807,7 @@ export const catalog: Catalog = {
|
|||||||
"Rename folder": "Map hernoemen",
|
"Rename folder": "Map hernoemen",
|
||||||
"Search: {query}": "Zoeken: {query}",
|
"Search: {query}": "Zoeken: {query}",
|
||||||
"No conversation selected": "Geen gesprek geselecteerd",
|
"No conversation selected": "Geen gesprek geselecteerd",
|
||||||
|
"No message selected": "Geen bericht geselecteerd",
|
||||||
"Drop here for the top level": "Hier neerzetten voor het hoogste niveau",
|
"Drop here for the top level": "Hier neerzetten voor het hoogste niveau",
|
||||||
|
|
||||||
// ── Longer prose ───────────────────────────────────────────────────
|
// ── Longer prose ───────────────────────────────────────────────────
|
||||||
@@ -815,6 +816,7 @@ export const catalog: Catalog = {
|
|||||||
"Open the Mail view to see all shortcuts.": "Open de E-mailweergave om alle sneltoetsen te zien.",
|
"Open the Mail view to see all shortcuts.": "Open de E-mailweergave om alle sneltoetsen te zien.",
|
||||||
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Sneltoetsen in Gmail-stijl staan altijd aan. Druk overal op {key} om deze lijst te zien.",
|
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Sneltoetsen in Gmail-stijl staan altijd aan. Druk overal op {key} om deze lijst te zien.",
|
||||||
"Select a conversation to read it here · Press {key} for shortcuts": "Selecteer een gesprek om het hier te lezen · {key} voor sneltoetsen",
|
"Select a conversation to read it here · Press {key} for shortcuts": "Selecteer een gesprek om het hier te lezen · {key} voor sneltoetsen",
|
||||||
|
"Select a message to read it here · Press {key} for shortcuts": "Selecteer een bericht om het hier te lezen · {key} voor sneltoetsen",
|
||||||
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Tip: druk op {key} bij een gesprek om labels toe te wijzen. Zoek met {operator}.",
|
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Tip: druk op {key} bij een gesprek om labels toe te wijzen. Zoek met {operator}.",
|
||||||
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Een snelle, prettige, opensource webmail voor {server}, gebouwd op JMAP.",
|
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Een snelle, prettige, opensource webmail voor {server}, gebouwd op JMAP.",
|
||||||
"Defaults for the calendar views and new events.": "Standaardwaarden voor de agendaweergaven en nieuwe afspraken.",
|
"Defaults for the calendar views and new events.": "Standaardwaarden voor de agendaweergaven en nieuwe afspraken.",
|
||||||
|
|||||||
@@ -814,6 +814,7 @@ export const catalog: Catalog = {
|
|||||||
"Rename folder": "Renomear a pasta",
|
"Rename folder": "Renomear a pasta",
|
||||||
"Search: {query}": "Pesquisa: {query}",
|
"Search: {query}": "Pesquisa: {query}",
|
||||||
"No conversation selected": "Nenhuma conversa selecionada",
|
"No conversation selected": "Nenhuma conversa selecionada",
|
||||||
|
"No message selected": "Nenhuma mensagem selecionada",
|
||||||
"Drop here for the top level": "Solte aqui para o nível superior",
|
"Drop here for the top level": "Solte aqui para o nível superior",
|
||||||
|
|
||||||
// ── Longer prose ───────────────────────────────────────────────────
|
// ── Longer prose ───────────────────────────────────────────────────
|
||||||
@@ -822,6 +823,7 @@ export const catalog: Catalog = {
|
|||||||
"Open the Mail view to see all shortcuts.": "Abra a visualização de E-mail para ver todos os atalhos.",
|
"Open the Mail view to see all shortcuts.": "Abra a visualização de E-mail para ver todos os atalhos.",
|
||||||
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Os atalhos no estilo do Gmail estão sempre ativos. Pressione {key} em qualquer lugar para ver esta lista.",
|
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Os atalhos no estilo do Gmail estão sempre ativos. Pressione {key} em qualquer lugar para ver esta lista.",
|
||||||
"Select a conversation to read it here · Press {key} for shortcuts": "Selecione uma conversa para lê-la aqui · {key} para os atalhos",
|
"Select a conversation to read it here · Press {key} for shortcuts": "Selecione uma conversa para lê-la aqui · {key} para os atalhos",
|
||||||
|
"Select a message to read it here · Press {key} for shortcuts": "Selecione uma mensagem para lê-la aqui · {key} para os atalhos",
|
||||||
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Dica: pressione {key} em uma conversa para aplicar marcadores. Pesquise com {operator}.",
|
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Dica: pressione {key} em uma conversa para aplicar marcadores. Pesquise com {operator}.",
|
||||||
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Um webmail livre, rápido e agradável para {server}, feito sobre JMAP.",
|
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Um webmail livre, rápido e agradável para {server}, feito sobre JMAP.",
|
||||||
"Defaults for the calendar views and new events.": "Padrões das visualizações da agenda e dos eventos novos.",
|
"Defaults for the calendar views and new events.": "Padrões das visualizações da agenda e dos eventos novos.",
|
||||||
|
|||||||
@@ -813,6 +813,7 @@ export const catalog: Catalog = {
|
|||||||
"Rename folder": "Переименовать папку",
|
"Rename folder": "Переименовать папку",
|
||||||
"Search: {query}": "Поиск: {query}",
|
"Search: {query}": "Поиск: {query}",
|
||||||
"No conversation selected": "Цепочка не выбрана",
|
"No conversation selected": "Цепочка не выбрана",
|
||||||
|
"No message selected": "Письмо не выбрано",
|
||||||
"Drop here for the top level": "Перетащите сюда, чтобы вынести на верхний уровень",
|
"Drop here for the top level": "Перетащите сюда, чтобы вынести на верхний уровень",
|
||||||
|
|
||||||
// ── Longer prose ───────────────────────────────────────────────────
|
// ── Longer prose ───────────────────────────────────────────────────
|
||||||
@@ -821,6 +822,7 @@ export const catalog: Catalog = {
|
|||||||
"Open the Mail view to see all shortcuts.": "Откройте раздел «Почта», чтобы увидеть все сочетания клавиш.",
|
"Open the Mail view to see all shortcuts.": "Откройте раздел «Почта», чтобы увидеть все сочетания клавиш.",
|
||||||
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Сочетания клавиш в стиле Gmail всегда включены. Нажмите {key} в любом месте, чтобы увидеть этот список.",
|
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Сочетания клавиш в стиле Gmail всегда включены. Нажмите {key} в любом месте, чтобы увидеть этот список.",
|
||||||
"Select a conversation to read it here · Press {key} for shortcuts": "Выберите цепочку, чтобы прочитать её здесь · {key} — сочетания клавиш",
|
"Select a conversation to read it here · Press {key} for shortcuts": "Выберите цепочку, чтобы прочитать её здесь · {key} — сочетания клавиш",
|
||||||
|
"Select a message to read it here · Press {key} for shortcuts": "Выберите письмо, чтобы прочитать его здесь · {key} — сочетания клавиш",
|
||||||
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Совет: нажмите {key} на цепочке, чтобы присвоить ярлыки. Ищите через {operator}.",
|
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Совет: нажмите {key} на цепочке, чтобы присвоить ярлыки. Ищите через {operator}.",
|
||||||
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Быстрая и удобная веб-почта с открытым кодом для {server}, построенная на JMAP.",
|
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Быстрая и удобная веб-почта с открытым кодом для {server}, построенная на JMAP.",
|
||||||
"Defaults for the calendar views and new events.": "Значения по умолчанию для видов календаря и новых событий.",
|
"Defaults for the calendar views and new events.": "Значения по умолчанию для видов календаря и новых событий.",
|
||||||
|
|||||||
@@ -807,6 +807,7 @@ export const catalog: Catalog = {
|
|||||||
"Rename folder": "Перейменувати теку",
|
"Rename folder": "Перейменувати теку",
|
||||||
"Search: {query}": "Пошук: {query}",
|
"Search: {query}": "Пошук: {query}",
|
||||||
"No conversation selected": "Листування не вибрано",
|
"No conversation selected": "Листування не вибрано",
|
||||||
|
"No message selected": "Лист не вибрано",
|
||||||
"Drop here for the top level": "Перетягніть сюди, щоб винести на верхній рівень",
|
"Drop here for the top level": "Перетягніть сюди, щоб винести на верхній рівень",
|
||||||
|
|
||||||
// ── Longer prose ───────────────────────────────────────────────────
|
// ── Longer prose ───────────────────────────────────────────────────
|
||||||
@@ -815,6 +816,7 @@ export const catalog: Catalog = {
|
|||||||
"Open the Mail view to see all shortcuts.": "Відкрийте розділ «Пошта», щоб побачити всі сполучення клавіш.",
|
"Open the Mail view to see all shortcuts.": "Відкрийте розділ «Пошта», щоб побачити всі сполучення клавіш.",
|
||||||
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Сполучення клавіш у стилі Gmail завжди увімкнено. Натисніть {key} будь-де, щоб побачити цей список.",
|
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Сполучення клавіш у стилі Gmail завжди увімкнено. Натисніть {key} будь-де, щоб побачити цей список.",
|
||||||
"Select a conversation to read it here · Press {key} for shortcuts": "Виберіть листування, щоб прочитати його тут · {key} — сполучення клавіш",
|
"Select a conversation to read it here · Press {key} for shortcuts": "Виберіть листування, щоб прочитати його тут · {key} — сполучення клавіш",
|
||||||
|
"Select a message to read it here · Press {key} for shortcuts": "Виберіть лист, щоб прочитати його тут · {key} — сполучення клавіш",
|
||||||
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Порада: натисніть {key} на листуванні, щоб додати мітки. Шукайте через {operator}.",
|
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "Порада: натисніть {key} на листуванні, щоб додати мітки. Шукайте через {operator}.",
|
||||||
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Швидка та зручна вебпошта з відкритим кодом для {server}, побудована на JMAP.",
|
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "Швидка та зручна вебпошта з відкритим кодом для {server}, побудована на JMAP.",
|
||||||
"Defaults for the calendar views and new events.": "Значення за замовчуванням для виглядів календаря та нових подій.",
|
"Defaults for the calendar views and new events.": "Значення за замовчуванням для виглядів календаря та нових подій.",
|
||||||
|
|||||||
@@ -746,6 +746,7 @@ export const catalog: Catalog = {
|
|||||||
"Open the Mail view to see all shortcuts.": "打开邮件视图以查看全部快捷键。",
|
"Open the Mail view to see all shortcuts.": "打开邮件视图以查看全部快捷键。",
|
||||||
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Gmail 风格的快捷键始终启用。在任意位置按 {key} 即可查看此列表。",
|
"Gmail-style shortcuts are always on. Press {key} anywhere to see this list.": "Gmail 风格的快捷键始终启用。在任意位置按 {key} 即可查看此列表。",
|
||||||
"Select a conversation to read it here · Press {key} for shortcuts": "选择一个会话即可在此阅读 · 按 {key} 查看快捷键",
|
"Select a conversation to read it here · Press {key} for shortcuts": "选择一个会话即可在此阅读 · 按 {key} 查看快捷键",
|
||||||
|
"Select a message to read it here · Press {key} for shortcuts": "选择一封邮件即可在此阅读 · 按 {key} 查看快捷键",
|
||||||
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "提示:在会话上按 {key} 可添加标签。使用 {operator} 搜索。",
|
"Tip: press {key} on a conversation to apply labels. Search with {operator}.": "提示:在会话上按 {key} 可添加标签。使用 {operator} 搜索。",
|
||||||
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "一款面向 {server} 的快速、友好的开源网页邮箱,基于 JMAP 构建。",
|
"A fast, friendly, open-source webmail for {server}, built on JMAP.": "一款面向 {server} 的快速、友好的开源网页邮箱,基于 JMAP 构建。",
|
||||||
|
|
||||||
@@ -842,6 +843,7 @@ export const catalog: Catalog = {
|
|||||||
"Not spam": "不是垃圾邮件",
|
"Not spam": "不是垃圾邮件",
|
||||||
"Nothing": "不执行任何操作",
|
"Nothing": "不执行任何操作",
|
||||||
"No conversation selected": "未选择会话",
|
"No conversation selected": "未选择会话",
|
||||||
|
"No message selected": "未选择邮件",
|
||||||
"Drop here for the top level": "拖放到此处可移至顶层",
|
"Drop here for the top level": "拖放到此处可移至顶层",
|
||||||
"Later today": "今天晚些时候",
|
"Later today": "今天晚些时候",
|
||||||
"Tomorrow morning": "明天上午",
|
"Tomorrow morning": "明天上午",
|
||||||
|
|||||||
@@ -107,15 +107,40 @@ export function MailView({ mailboxId, threadId, search }: { mailboxId?: string;
|
|||||||
if (mailboxesLoaded && mailboxId && mailboxId === scheduledId) void reconcile();
|
if (mailboxesLoaded && mailboxId && mailboxId === scheduledId) void reconcile();
|
||||||
}, [mailboxId, scheduledId, mailboxesLoaded, reconcile]);
|
}, [mailboxId, scheduledId, mailboxesLoaded, reconcile]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* With conversation view off, a row is a message rather than a thread, and
|
||||||
|
* opening one must show that message and highlight that row -- not its whole
|
||||||
|
* thread and every sibling row in the list.
|
||||||
|
*
|
||||||
|
* The thread id stays in the path, so loading is unchanged; the message rides
|
||||||
|
* in `m`. Putting it in the URL rather than in memory is what makes a reload
|
||||||
|
* or a shared link land back on the same message, and dropping the parameter
|
||||||
|
* degrades to the conversation, which is the right thing for a link sent to
|
||||||
|
* somebody whose setting differs.
|
||||||
|
*/
|
||||||
const openThread = useCallback(
|
const openThread = useCallback(
|
||||||
(tid: Id | null) => {
|
(tid: Id | null, messageId?: Id | null) => {
|
||||||
const base = search ? `/search` : `/mail/${mailboxId}`;
|
const base = search ? `/search` : `/mail/${mailboxId}`;
|
||||||
const qs = search ? `?q=${encodeURIComponent(q)}` : "";
|
const params = new URLSearchParams();
|
||||||
|
if (search) params.set("q", q);
|
||||||
|
if (tid && messageId) params.set("m", messageId);
|
||||||
|
const qs = params.size ? `?${params}` : "";
|
||||||
navigate(tid ? `${base}/${tid}${qs}` : `${base}${qs}`);
|
navigate(tid ? `${base}/${tid}${qs}` : `${base}${qs}`);
|
||||||
},
|
},
|
||||||
[navigate, search, mailboxId, q],
|
[navigate, search, mailboxId, q],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The message the URL singles out, if any. Only meaningful with conversation
|
||||||
|
* view off; ThreadView decides what to do when the id names nothing in the
|
||||||
|
* thread, since it is the part that knows what the thread holds.
|
||||||
|
*/
|
||||||
|
const openMessageId = useMemo(() => {
|
||||||
|
if (settings.conversationMode) return null;
|
||||||
|
const m = new URLSearchParams(searchStr).get("m");
|
||||||
|
return m || null;
|
||||||
|
}, [settings.conversationMode, searchStr]);
|
||||||
|
|
||||||
// Row ids in list + helpers for keyboard nav
|
// Row ids in list + helpers for keyboard nav
|
||||||
const ids = list?.ids ?? [];
|
const ids = list?.ids ?? [];
|
||||||
const emails = useMail((s) => s.emails);
|
const emails = useMail((s) => s.emails);
|
||||||
@@ -129,9 +154,17 @@ export function MailView({ mailboxId, threadId, search }: { mailboxId?: string;
|
|||||||
const i = ids.indexOf(focusId);
|
const i = ids.indexOf(focusId);
|
||||||
if (i >= 0) return i;
|
if (i >= 0) return i;
|
||||||
}
|
}
|
||||||
|
// A cold load has no focus yet. With conversation view off the URL names the
|
||||||
|
// row exactly; matching on the thread instead would land on whichever of its
|
||||||
|
// messages sorts first, so j/k and the scroll-into-view would start from the
|
||||||
|
// wrong row on any thread with more than one message in the folder.
|
||||||
|
if (openMessageId) {
|
||||||
|
const i = ids.indexOf(openMessageId);
|
||||||
|
if (i >= 0) return i;
|
||||||
|
}
|
||||||
if (threadId) return ids.findIndex((id) => rowThreadId(id) === threadId);
|
if (threadId) return ids.findIndex((id) => rowThreadId(id) === threadId);
|
||||||
return -1;
|
return -1;
|
||||||
}, [ids, focusId, threadId, rowThreadId]);
|
}, [ids, focusId, openMessageId, threadId, rowThreadId]);
|
||||||
|
|
||||||
/** Email ids affected by an action on rows (selection or focused/open row). */
|
/** Email ids affected by an action on rows (selection or focused/open row). */
|
||||||
const targetIds = useCallback(
|
const targetIds = useCallback(
|
||||||
@@ -339,9 +372,9 @@ export function MailView({ mailboxId, threadId, search }: { mailboxId?: string;
|
|||||||
void openDraft(e);
|
void openDraft(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
openThread(e.threadId);
|
openThread(e.threadId, settings.conversationMode ? null : rowId);
|
||||||
},
|
},
|
||||||
[emails, mailboxId, mailboxes, openThread, openDraft],
|
[emails, mailboxId, mailboxes, openThread, openDraft, settings.conversationMode],
|
||||||
);
|
);
|
||||||
|
|
||||||
const title = search ? translate("Search: {query}", { query: listQuery?.label ?? q }) : (mailboxId && mailboxDisplayName(mailboxes[mailboxId])) || translate("Mail");
|
const title = search ? translate("Search: {query}", { query: listQuery?.label ?? q }) : (mailboxId && mailboxDisplayName(mailboxes[mailboxId])) || translate("Mail");
|
||||||
@@ -373,6 +406,7 @@ export function MailView({ mailboxId, threadId, search }: { mailboxId?: string;
|
|||||||
title={title}
|
title={title}
|
||||||
list={list}
|
list={list}
|
||||||
openThreadId={threadId ?? null}
|
openThreadId={threadId ?? null}
|
||||||
|
openMessageId={openMessageId}
|
||||||
focusId={focusId}
|
focusId={focusId}
|
||||||
setFocusId={setFocusId}
|
setFocusId={setFocusId}
|
||||||
onOpen={onOpenRow}
|
onOpen={onOpenRow}
|
||||||
@@ -387,12 +421,24 @@ export function MailView({ mailboxId, threadId, search }: { mailboxId?: string;
|
|||||||
{showReading && (
|
{showReading && (
|
||||||
<div className="mail-reading-pane">
|
<div className="mail-reading-pane">
|
||||||
{threadId ? (
|
{threadId ? (
|
||||||
<ThreadView key={threadId} threadId={threadId} mailboxId={mailboxId ?? null} onBack={() => openThread(null)} actions={actions} onNavigate={(delta) => { const idx = currentRowIndex; const next = ids[idx + delta]; const t = next ? rowThreadId(next) : undefined; if (t) { setFocusId(next!); openThread(t); } }} hasPrev={currentRowIndex > 0} hasNext={currentRowIndex >= 0 && currentRowIndex < ids.length - 1} />
|
<ThreadView key={`${threadId}:${openMessageId ?? ""}`} threadId={threadId} messageId={openMessageId} mailboxId={mailboxId ?? null} onBack={() => openThread(null)} actions={actions} onNavigate={(delta) => { const idx = currentRowIndex; const next = ids[idx + delta]; const t = next ? rowThreadId(next) : undefined; if (t) { setFocusId(next!); openThread(t, settings.conversationMode ? null : next!); } }} hasPrev={currentRowIndex > 0} hasNext={currentRowIndex >= 0 && currentRowIndex < ids.length - 1} />
|
||||||
) : (
|
) : (
|
||||||
<div className="no-thread">
|
<div className="no-thread">
|
||||||
<img src={withBase("/img/logo.png")} alt="" />
|
<img src={withBase("/img/logo.png")} alt="" />
|
||||||
<div>{list?.total ? plural(list.total, { one: "{n} conversation", other: "{n} conversations" }) : translate("No conversation selected")}</div>
|
<div>
|
||||||
<div className="hint">{tNode("Select a conversation to read it here · Press {key} for shortcuts", { key: <kbd className="kbd">?</kbd> })}</div>
|
{list?.total
|
||||||
|
? settings.conversationMode
|
||||||
|
? plural(list.total, { one: "{n} conversation", other: "{n} conversations" })
|
||||||
|
: plural(list.total, { one: "{n} message", other: "{n} messages" })
|
||||||
|
: settings.conversationMode
|
||||||
|
? translate("No conversation selected")
|
||||||
|
: translate("No message selected")}
|
||||||
|
</div>
|
||||||
|
<div className="hint">
|
||||||
|
{settings.conversationMode
|
||||||
|
? tNode("Select a conversation to read it here · Press {key} for shortcuts", { key: <kbd className="kbd">?</kbd> })
|
||||||
|
: tNode("Select a message to read it here · Press {key} for shortcuts", { key: <kbd className="kbd">?</kbd> })}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { formatListDate } from "@/lib/format";
|
|||||||
import { mailboxDisplayName } from "@/lib/mailboxName";
|
import { mailboxDisplayName } from "@/lib/mailboxName";
|
||||||
import { groupByArchivePath, archivePath, type ArchiveGranularity } from "@/lib/archiveDate";
|
import { groupByArchivePath, archivePath, type ArchiveGranularity } from "@/lib/archiveDate";
|
||||||
import { canEmpty, confirmAndEmpty, emptyLabel } from "@/lib/emptyFolder";
|
import { canEmpty, confirmAndEmpty, emptyLabel } from "@/lib/emptyFolder";
|
||||||
|
import { rowIsOpen } from "@/lib/openMessage";
|
||||||
import { displayName, shortName } from "@/lib/address";
|
import { displayName, shortName } from "@/lib/address";
|
||||||
import { Avatar, Empty, useIsMobile, useIsTouch } from "@/ui/misc";
|
import { Avatar, Empty, useIsMobile, useIsTouch } from "@/ui/misc";
|
||||||
import { rowClick } from "@/lib/listSelection";
|
import { rowClick } from "@/lib/listSelection";
|
||||||
@@ -53,6 +54,12 @@ interface Props {
|
|||||||
title: string;
|
title: string;
|
||||||
list: ListState | null;
|
list: ListState | null;
|
||||||
openThreadId: Id | null;
|
openThreadId: Id | null;
|
||||||
|
/**
|
||||||
|
* With conversation view off, the one message the reading pane is showing.
|
||||||
|
* The row highlight follows this instead of the thread, or every message in
|
||||||
|
* a thread lights up when one of them is opened.
|
||||||
|
*/
|
||||||
|
openMessageId: Id | null;
|
||||||
focusId: Id | null;
|
focusId: Id | null;
|
||||||
setFocusId: (id: Id | null) => void;
|
setFocusId: (id: Id | null) => void;
|
||||||
onOpen: (rowId: Id) => void;
|
onOpen: (rowId: Id) => void;
|
||||||
@@ -61,7 +68,7 @@ interface Props {
|
|||||||
isSearch: boolean;
|
isSearch: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MessageList({ title, list, openThreadId, focusId, setFocusId, onOpen, actions, mailboxId, isSearch }: Props) {
|
export function MessageList({ title, list, openThreadId, openMessageId, focusId, setFocusId, onOpen, actions, mailboxId, isSearch }: Props) {
|
||||||
const [, navigate] = useLocation();
|
const [, navigate] = useLocation();
|
||||||
const emails = useMail((s) => s.emails);
|
const emails = useMail((s) => s.emails);
|
||||||
const threads = useMail((s) => s.threads);
|
const threads = useMail((s) => s.threads);
|
||||||
@@ -485,7 +492,7 @@ export function MessageList({ title, list, openThreadId, focusId, setFocusId, on
|
|||||||
height={vi.size}
|
height={vi.size}
|
||||||
selected={Boolean(selected[id])}
|
selected={Boolean(selected[id])}
|
||||||
focused={focusId === id}
|
focused={focusId === id}
|
||||||
open={openThreadId === e.threadId}
|
open={rowIsOpen(id, e.threadId, openMessageId, openThreadId)}
|
||||||
twoLine={twoLine}
|
twoLine={twoLine}
|
||||||
showAvatar={settings.showAvatars}
|
showAvatar={settings.showAvatars}
|
||||||
showPreview={settings.showPreview}
|
showPreview={settings.showPreview}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { AlertOctagon, Archive, ArrowLeft, ChevronDown, ChevronUp, FolderInput, Forward, Mail, MailOpen, MailPlus, MoreVertical, Printer, Reply, ReplyAll, ShieldCheck, Star, Tag, Trash2, Download , Paperclip} from "lucide-react";
|
import { AlertOctagon, Archive, ArrowLeft, ChevronDown, ChevronUp, FolderInput, Forward, Mail, MailOpen, MailPlus, MoreVertical, Printer, Reply, ReplyAll, ShieldCheck, Star, Tag, Trash2, Download , Paperclip} from "lucide-react";
|
||||||
import { useMail } from "@/store/mail";
|
import { useMail } from "@/store/mail";
|
||||||
|
import { visibleMessages } from "@/lib/openMessage";
|
||||||
import { useSettings } from "@/store/settings";
|
import { useSettings } from "@/store/settings";
|
||||||
import { useCompose } from "@/store/compose";
|
import { useCompose } from "@/store/compose";
|
||||||
import type { Email, Id } from "@/jmap/types";
|
import type { Email, Id } from "@/jmap/types";
|
||||||
@@ -25,9 +26,15 @@ interface Props {
|
|||||||
onNavigate: (delta: number) => void;
|
onNavigate: (delta: number) => void;
|
||||||
hasPrev: boolean;
|
hasPrev: boolean;
|
||||||
hasNext: boolean;
|
hasNext: boolean;
|
||||||
|
/**
|
||||||
|
* With conversation view off, the single message to show. The thread is still
|
||||||
|
* what loads -- one request, and the reply/forward paths keep the context
|
||||||
|
* they need -- but only this message is rendered.
|
||||||
|
*/
|
||||||
|
messageId?: Id | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ThreadView({ threadId, mailboxId, onBack, actions, onNavigate, hasPrev, hasNext }: Props) {
|
export function ThreadView({ threadId, mailboxId, onBack, actions, onNavigate, hasPrev, hasNext, messageId = null }: Props) {
|
||||||
const loadThread = useMail((s) => s.loadThread);
|
const loadThread = useMail((s) => s.loadThread);
|
||||||
const thread = useMail((s) => s.threads[threadId]);
|
const thread = useMail((s) => s.threads[threadId]);
|
||||||
const emails = useMail((s) => s.emails);
|
const emails = useMail((s) => s.emails);
|
||||||
@@ -82,8 +89,9 @@ export function ThreadView({ threadId, mailboxId, onBack, actions, onNavigate, h
|
|||||||
if (junk && e.mailboxIds[junk]) return false;
|
if (junk && e.mailboxIds[junk]) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
return (filtered.length ? filtered : all).sort((a, b) => a.receivedAt.localeCompare(b.receivedAt));
|
const shown = filtered.length ? filtered : all;
|
||||||
}, [thread, emails, fullIds, mailboxId]);
|
return visibleMessages(shown, messageId).sort((a, b) => a.receivedAt.localeCompare(b.receivedAt));
|
||||||
|
}, [thread, emails, fullIds, mailboxId, messageId]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Which messages were unread when this conversation was opened.
|
* Which messages were unread when this conversation was opened.
|
||||||
|
|||||||
Reference in New Issue
Block a user