Takes the flat module count from 66 to 42, continuing what admin/ and
calendar/ started.
lib/mailbox/ archiveDate, emptyFolder, folderMove, labelTree,
mailboxName, mailboxRoute
lib/sieve/ sieve, sieveApply, sieveFolders
lib/input/ keyboard, swipe, touch, listSelection, dropUpload
lib/notify/ notify, webpush, webpushEnable
lib/sw/ swCache, swFacts, staleBuild
lib/text/ html, markdown, text, emlName
FOUR THINGS THE FILENAMES GET WRONG, each checked by reading the file
rather than trusting what it is called:
- appFolder is not a mailbox. It is the `ihasmail` folder in JMAP
*Files*, where the client keeps signature images and synced settings.
It stays flat.
- format holds no formatting of text. It re-exports the date and clock
formatters, so it belongs with dates/datetime, not with text/.
- preview is the file viewer deciding what it can show without
downloading, and source is where to point someone asking for this
instance's AGPL source. Neither is about text.
- notify is not Web Push. It is the tab title, the favicon badge and
the new-mail sound -- in-app notification, which is why it sits with
webpush rather than under sw/ with the service worker's own concerns.
threadScroll stays flat too: it decides where a conversation opens, which
is view state rather than a gesture, and input/ is honest only if
everything in it interprets something the reader did.
No behavior change. Almost every reference was on the @/ alias; eight
relative imports in files that did not move, or that moved away from a
sibling, needed rewriting by hand.
91 lines
4.2 KiB
TypeScript
91 lines
4.2 KiB
TypeScript
/**
|
|
* What a swipe on a message row does, and what it should say it is about to do.
|
|
*
|
|
* The reader picks one action for each direction in Settings, but an action is
|
|
* not a fixed thing: "delete" out of Deleted Items is permanent, "report spam"
|
|
* inside Junk Mail is the opposite request, and "archive" while looking at the
|
|
* archive is nothing at all. The strip revealed behind the row has to name the
|
|
* thing that will actually happen, in the folder it is happening in -- a row
|
|
* that slides open to reveal the word "Archive" and then does nothing is worse
|
|
* than one that does not slide.
|
|
*
|
|
* So a direction with no meaning here resolves to `null`, and a null direction
|
|
* is one the row simply will not move in.
|
|
*/
|
|
|
|
export type SwipeAction = "archive" | "delete" | "spam" | "read" | "star" | "move" | "none";
|
|
|
|
export interface SwipeContext {
|
|
/** The role of the folder on screen, where it has one. */
|
|
role?: string | null;
|
|
/** Whether the row is unread — "mark as read" is a toggle, and says so. */
|
|
unread: boolean;
|
|
starred: boolean;
|
|
}
|
|
|
|
/**
|
|
* Which glyph the strip shows. Named for the state it is offering rather than
|
|
* the setting it came from: "mark as unread" and "not spam" are the same two
|
|
* settings as their opposites but nothing like the same icon, and a strip that
|
|
* says "Not spam" beside a spam icon is asking to be misread at a glance.
|
|
*/
|
|
export type SwipeIcon = "archive" | "delete" | "spam" | "not-spam" | "read" | "unread" | "star" | "unstar" | "move";
|
|
|
|
export interface SwipeDescriptor {
|
|
action: Exclude<SwipeAction, "none">;
|
|
label: string;
|
|
icon: SwipeIcon;
|
|
/** Which color the strip behind the row takes. */
|
|
tone: "danger" | "warn" | "accent" | "neutral";
|
|
/**
|
|
* Whether firing it takes the row out of the list. Those slide the rest of
|
|
* the way off before they fire, so the message is gone from under the finger
|
|
* rather than snapping home and then vanishing a frame later.
|
|
*/
|
|
removes: boolean;
|
|
/**
|
|
* For the two actions that are toggles, the state this swipe sets — so the
|
|
* caller fires exactly what the strip promised. Read back off the row
|
|
* instead and a slow finger can invert it: the strip that said "Mark as
|
|
* read" would mark as unread if a push update landed mid-gesture.
|
|
*/
|
|
on?: boolean;
|
|
}
|
|
|
|
export function describeSwipe(action: SwipeAction, ctx: SwipeContext): SwipeDescriptor | null {
|
|
switch (action) {
|
|
case "archive":
|
|
// Archiving out of the archive is the one no-op worth refusing outright.
|
|
return ctx.role === "archive" ? null : { action, label: "Archive", icon: "archive", tone: "accent", removes: true };
|
|
case "delete":
|
|
return { action, label: ctx.role === "trash" ? "Delete forever" : "Delete", icon: "delete", tone: "danger", removes: true };
|
|
case "spam":
|
|
// Nothing you wrote is spam you received, so the gesture stays inert in
|
|
// the two folders that hold your own mail.
|
|
if (ctx.role === "drafts" || ctx.role === "sent") return null;
|
|
return ctx.role === "junk"
|
|
? { action, label: "Not spam", icon: "not-spam", tone: "warn", removes: true }
|
|
: { action, label: "Report spam", icon: "spam", tone: "warn", removes: true };
|
|
case "read":
|
|
return { action, label: ctx.unread ? "Mark as read" : "Mark as unread", icon: ctx.unread ? "read" : "unread", tone: "neutral", removes: false, on: ctx.unread };
|
|
case "star":
|
|
return { action, label: ctx.starred ? "Remove star" : "Add star", icon: ctx.starred ? "unstar" : "star", tone: "warn", removes: false, on: !ctx.starred };
|
|
case "move":
|
|
// The folder picker opens over the list, so the row comes home first.
|
|
return { action, label: "Move to…", icon: "move", tone: "accent", removes: false };
|
|
case "none":
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** The Settings picker's options, in the order they are offered. */
|
|
export const SWIPE_CHOICES: ReadonlyArray<{ value: SwipeAction; label: string }> = [
|
|
{ value: "archive", label: "Archive" },
|
|
{ value: "delete", label: "Delete" },
|
|
{ value: "read", label: "Mark as read / unread" },
|
|
{ value: "star", label: "Star / unstar" },
|
|
{ value: "spam", label: "Report spam / not spam" },
|
|
{ value: "move", label: "Move to…" },
|
|
{ value: "none", label: "Nothing" },
|
|
];
|