@1kbirds/chidori-mock-gmail 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -0
- package/dist/api.d.ts +14 -0
- package/dist/api.d.ts.map +1 -0
- package/dist/api.js +139 -0
- package/dist/client.d.ts +228 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +50 -0
- package/dist/errors.d.ts +19 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +25 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/seed.d.ts +6 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +127 -0
- package/dist/service.d.ts +41 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +372 -0
- package/dist/state.d.ts +22 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +139 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/ui/GmailMockApp.d.ts +7 -0
- package/dist/ui/GmailMockApp.d.ts.map +1 -0
- package/dist/ui/GmailMockApp.js +93 -0
- package/dist/ui/dev.d.ts +2 -0
- package/dist/ui/dev.d.ts.map +1 -0
- package/dist/ui/dev.js +11 -0
- package/dist/ui/index.d.ts +3 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/styles.css +340 -0
- package/package.json +56 -0
- package/src/__tests__/service.test.ts +120 -0
- package/src/api.ts +157 -0
- package/src/client.ts +54 -0
- package/src/errors.ts +29 -0
- package/src/index.ts +12 -0
- package/src/seed.ts +143 -0
- package/src/service.ts +405 -0
- package/src/state.ts +159 -0
- package/src/types.ts +149 -0
- package/src/ui/GmailMockApp.tsx +236 -0
- package/src/ui/dev.tsx +16 -0
- package/src/ui/index.ts +2 -0
- package/src/ui/styles.css +340 -0
package/dist/state.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const defaultUser = {
|
|
2
|
+
id: "me",
|
|
3
|
+
emailAddress: "user@example.com",
|
|
4
|
+
displayName: "Mock User",
|
|
5
|
+
};
|
|
6
|
+
const systemLabels = [
|
|
7
|
+
{ id: "INBOX", name: "Inbox" },
|
|
8
|
+
{ id: "SENT", name: "Sent" },
|
|
9
|
+
{ id: "DRAFT", name: "Drafts" },
|
|
10
|
+
{ id: "TRASH", name: "Trash" },
|
|
11
|
+
{ id: "SPAM", name: "Spam" },
|
|
12
|
+
{ id: "STARRED", name: "Starred" },
|
|
13
|
+
{ id: "UNREAD", name: "Unread" },
|
|
14
|
+
{ id: "IMPORTANT", name: "Important" },
|
|
15
|
+
{ id: "CATEGORY_PRIMARY", name: "Primary" },
|
|
16
|
+
];
|
|
17
|
+
export function createStore(options = {}) {
|
|
18
|
+
const fixedNow = options.now ? new Date(options.now) : undefined;
|
|
19
|
+
const user = { ...defaultUser, ...options.currentUser };
|
|
20
|
+
const counters = new Map();
|
|
21
|
+
const store = {
|
|
22
|
+
users: new Map([[user.id, user]]),
|
|
23
|
+
currentUserId: user.id,
|
|
24
|
+
labels: new Map(defaultLabels().map((label) => [label.id, label])),
|
|
25
|
+
messages: new Map(),
|
|
26
|
+
drafts: new Map(),
|
|
27
|
+
historyId: 1,
|
|
28
|
+
now: () => new Date(fixedNow ?? new Date()),
|
|
29
|
+
nextId: (prefix) => {
|
|
30
|
+
const next = (counters.get(prefix) ?? 0) + 1;
|
|
31
|
+
counters.set(prefix, next);
|
|
32
|
+
return `${prefix}_${next.toString(36)}`;
|
|
33
|
+
},
|
|
34
|
+
listeners: new Set(),
|
|
35
|
+
};
|
|
36
|
+
if (options.seed) {
|
|
37
|
+
applySeed(store, options.seed);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
recomputeLabelStats(store);
|
|
41
|
+
}
|
|
42
|
+
return store;
|
|
43
|
+
}
|
|
44
|
+
export function defaultLabels() {
|
|
45
|
+
return systemLabels.map((label) => ({
|
|
46
|
+
id: label.id,
|
|
47
|
+
name: label.name,
|
|
48
|
+
type: "system",
|
|
49
|
+
messageListVisibility: "show",
|
|
50
|
+
labelListVisibility: "labelShow",
|
|
51
|
+
messagesTotal: 0,
|
|
52
|
+
messagesUnread: 0,
|
|
53
|
+
threadsTotal: 0,
|
|
54
|
+
threadsUnread: 0,
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
export function snapshot(store) {
|
|
58
|
+
recomputeLabelStats(store);
|
|
59
|
+
return {
|
|
60
|
+
users: Array.from(store.users.values()).map(clone),
|
|
61
|
+
currentUserId: store.currentUserId,
|
|
62
|
+
labels: Array.from(store.labels.values()).map(clone),
|
|
63
|
+
messages: Array.from(store.messages.values()).map(clone),
|
|
64
|
+
drafts: Array.from(store.drafts.values()).map(clone),
|
|
65
|
+
historyId: String(store.historyId),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export function applySnapshot(store, next) {
|
|
69
|
+
store.users = new Map(next.users.map((user) => [user.id, clone(user)]));
|
|
70
|
+
store.currentUserId = next.currentUserId;
|
|
71
|
+
store.labels = new Map(next.labels.map((label) => [label.id, clone(label)]));
|
|
72
|
+
store.messages = new Map(next.messages.map((message) => [message.id, clone(message)]));
|
|
73
|
+
store.drafts = new Map(next.drafts.map((draft) => [draft.id, clone(draft)]));
|
|
74
|
+
store.historyId = Number(next.historyId);
|
|
75
|
+
recomputeLabelStats(store);
|
|
76
|
+
emit(store);
|
|
77
|
+
}
|
|
78
|
+
export function applySeed(store, seed) {
|
|
79
|
+
applySnapshot(store, {
|
|
80
|
+
users: seed.users ?? Array.from(store.users.values()),
|
|
81
|
+
currentUserId: seed.currentUserId ?? seed.users?.[0]?.id ?? store.currentUserId,
|
|
82
|
+
labels: seed.labels ?? defaultLabels(),
|
|
83
|
+
messages: seed.messages ?? [],
|
|
84
|
+
drafts: seed.drafts ?? [],
|
|
85
|
+
historyId: "1",
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
export function bumpHistory(store) {
|
|
89
|
+
store.historyId += 1;
|
|
90
|
+
return String(store.historyId);
|
|
91
|
+
}
|
|
92
|
+
export function emit(store) {
|
|
93
|
+
recomputeLabelStats(store);
|
|
94
|
+
for (const listener of store.listeners) {
|
|
95
|
+
listener();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function recomputeLabelStats(store) {
|
|
99
|
+
for (const label of store.labels.values()) {
|
|
100
|
+
label.messagesTotal = 0;
|
|
101
|
+
label.messagesUnread = 0;
|
|
102
|
+
label.threadsTotal = 0;
|
|
103
|
+
label.threadsUnread = 0;
|
|
104
|
+
}
|
|
105
|
+
const threadIdsByLabel = new Map();
|
|
106
|
+
const unreadThreadIdsByLabel = new Map();
|
|
107
|
+
for (const message of store.messages.values()) {
|
|
108
|
+
for (const labelId of message.labelIds) {
|
|
109
|
+
const label = store.labels.get(labelId);
|
|
110
|
+
if (!label)
|
|
111
|
+
continue;
|
|
112
|
+
label.messagesTotal += 1;
|
|
113
|
+
if (message.labelIds.includes("UNREAD")) {
|
|
114
|
+
label.messagesUnread += 1;
|
|
115
|
+
}
|
|
116
|
+
if (!threadIdsByLabel.has(labelId))
|
|
117
|
+
threadIdsByLabel.set(labelId, new Set());
|
|
118
|
+
threadIdsByLabel.get(labelId).add(message.threadId);
|
|
119
|
+
if (message.labelIds.includes("UNREAD")) {
|
|
120
|
+
if (!unreadThreadIdsByLabel.has(labelId))
|
|
121
|
+
unreadThreadIdsByLabel.set(labelId, new Set());
|
|
122
|
+
unreadThreadIdsByLabel.get(labelId).add(message.threadId);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
for (const [labelId, threadIds] of threadIdsByLabel) {
|
|
127
|
+
const label = store.labels.get(labelId);
|
|
128
|
+
if (label)
|
|
129
|
+
label.threadsTotal = threadIds.size;
|
|
130
|
+
}
|
|
131
|
+
for (const [labelId, threadIds] of unreadThreadIdsByLabel) {
|
|
132
|
+
const label = store.labels.get(labelId);
|
|
133
|
+
if (label)
|
|
134
|
+
label.threadsUnread = threadIds.size;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export function clone(value) {
|
|
138
|
+
return structuredClone(value);
|
|
139
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export type SystemLabelId = "INBOX" | "SENT" | "DRAFT" | "TRASH" | "SPAM" | "STARRED" | "UNREAD" | "IMPORTANT" | "CATEGORY_PRIMARY";
|
|
2
|
+
export type LabelType = "system" | "user";
|
|
3
|
+
export interface GmailUser {
|
|
4
|
+
id: string;
|
|
5
|
+
emailAddress: string;
|
|
6
|
+
displayName: string;
|
|
7
|
+
}
|
|
8
|
+
export interface GmailLabel {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
type: LabelType;
|
|
12
|
+
messageListVisibility?: "show" | "hide";
|
|
13
|
+
labelListVisibility?: "labelShow" | "labelHide";
|
|
14
|
+
color?: {
|
|
15
|
+
textColor: string;
|
|
16
|
+
backgroundColor: string;
|
|
17
|
+
};
|
|
18
|
+
messagesTotal: number;
|
|
19
|
+
messagesUnread: number;
|
|
20
|
+
threadsTotal: number;
|
|
21
|
+
threadsUnread: number;
|
|
22
|
+
}
|
|
23
|
+
export interface GmailHeader {
|
|
24
|
+
name: string;
|
|
25
|
+
value: string;
|
|
26
|
+
}
|
|
27
|
+
export interface GmailMessagePartBody {
|
|
28
|
+
attachmentId?: string;
|
|
29
|
+
size: number;
|
|
30
|
+
data?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface GmailMessagePart {
|
|
33
|
+
partId: string;
|
|
34
|
+
mimeType: string;
|
|
35
|
+
filename?: string;
|
|
36
|
+
headers: GmailHeader[];
|
|
37
|
+
body: GmailMessagePartBody;
|
|
38
|
+
parts?: GmailMessagePart[];
|
|
39
|
+
}
|
|
40
|
+
export interface GmailAttachment {
|
|
41
|
+
id: string;
|
|
42
|
+
filename: string;
|
|
43
|
+
mimeType: string;
|
|
44
|
+
size: number;
|
|
45
|
+
data?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface GmailMessage {
|
|
48
|
+
id: string;
|
|
49
|
+
threadId: string;
|
|
50
|
+
labelIds: string[];
|
|
51
|
+
snippet: string;
|
|
52
|
+
historyId: string;
|
|
53
|
+
internalDate: string;
|
|
54
|
+
payload: GmailMessagePart;
|
|
55
|
+
sizeEstimate: number;
|
|
56
|
+
raw?: string;
|
|
57
|
+
attachments?: GmailAttachment[];
|
|
58
|
+
}
|
|
59
|
+
export interface GmailThread {
|
|
60
|
+
id: string;
|
|
61
|
+
historyId: string;
|
|
62
|
+
snippet: string;
|
|
63
|
+
messages: GmailMessage[];
|
|
64
|
+
}
|
|
65
|
+
export interface GmailDraft {
|
|
66
|
+
id: string;
|
|
67
|
+
message: GmailMessage;
|
|
68
|
+
}
|
|
69
|
+
export interface GmailProfile {
|
|
70
|
+
emailAddress: string;
|
|
71
|
+
messagesTotal: number;
|
|
72
|
+
threadsTotal: number;
|
|
73
|
+
historyId: string;
|
|
74
|
+
}
|
|
75
|
+
export interface GmailMockOptions {
|
|
76
|
+
currentUser?: Partial<GmailUser>;
|
|
77
|
+
now?: string | Date;
|
|
78
|
+
seed?: GmailSeed;
|
|
79
|
+
}
|
|
80
|
+
export interface GmailSeed {
|
|
81
|
+
users?: GmailUser[];
|
|
82
|
+
currentUserId?: string;
|
|
83
|
+
labels?: GmailLabel[];
|
|
84
|
+
messages?: GmailMessage[];
|
|
85
|
+
drafts?: GmailDraft[];
|
|
86
|
+
}
|
|
87
|
+
export interface GmailSnapshot {
|
|
88
|
+
users: GmailUser[];
|
|
89
|
+
currentUserId: string;
|
|
90
|
+
labels: GmailLabel[];
|
|
91
|
+
messages: GmailMessage[];
|
|
92
|
+
drafts: GmailDraft[];
|
|
93
|
+
historyId: string;
|
|
94
|
+
}
|
|
95
|
+
export interface GmailListResponse<T> {
|
|
96
|
+
resultSizeEstimate: number;
|
|
97
|
+
nextPageToken?: string;
|
|
98
|
+
messages?: T[];
|
|
99
|
+
threads?: T[];
|
|
100
|
+
labels?: T[];
|
|
101
|
+
drafts?: T[];
|
|
102
|
+
}
|
|
103
|
+
export interface GmailModifyRequest {
|
|
104
|
+
addLabelIds?: string[];
|
|
105
|
+
removeLabelIds?: string[];
|
|
106
|
+
}
|
|
107
|
+
export interface GmailSendRequest {
|
|
108
|
+
raw?: string;
|
|
109
|
+
message?: Partial<GmailMessage>;
|
|
110
|
+
to?: string[];
|
|
111
|
+
cc?: string[];
|
|
112
|
+
bcc?: string[];
|
|
113
|
+
subject?: string;
|
|
114
|
+
body?: string;
|
|
115
|
+
attachments?: GmailAttachment[];
|
|
116
|
+
}
|
|
117
|
+
export interface GmailSearchOptions {
|
|
118
|
+
q?: string;
|
|
119
|
+
labelIds?: string[];
|
|
120
|
+
includeSpamTrash?: boolean;
|
|
121
|
+
maxResults?: number;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { GmailMock } from "../service";
|
|
2
|
+
import "./styles.css";
|
|
3
|
+
export interface GmailMockAppProps {
|
|
4
|
+
mock: GmailMock;
|
|
5
|
+
}
|
|
6
|
+
export declare function GmailMockApp({ mock }: GmailMockAppProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=GmailMockApp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GmailMockApp.d.ts","sourceRoot":"","sources":["../../src/ui/GmailMockApp.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,cAAc,CAAC;AAEtB,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,SAAS,CAAC;CACjB;AAWD,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,EAAE,iBAAiB,2CAsMvD"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { demoSeed } from "../seed";
|
|
4
|
+
import { decodeBase64Url } from "../service";
|
|
5
|
+
import "./styles.css";
|
|
6
|
+
const primaryLabels = ["INBOX", "STARRED", "SENT", "DRAFT", "TRASH", "SPAM"];
|
|
7
|
+
export function GmailMockApp({ mock }) {
|
|
8
|
+
const [state, setState] = useState(() => mock.snapshot());
|
|
9
|
+
useEffect(() => mock.subscribe(() => setState(mock.snapshot())), [mock]);
|
|
10
|
+
const [activeLabel, setActiveLabel] = useState("INBOX");
|
|
11
|
+
const [query, setQuery] = useState("");
|
|
12
|
+
const [selectedMessageId, setSelectedMessageId] = useState(null);
|
|
13
|
+
const [compose, setCompose] = useState(null);
|
|
14
|
+
const label = state.labels.find((item) => item.id === activeLabel);
|
|
15
|
+
const messages = useMemo(() => state.messages
|
|
16
|
+
.filter((message) => activeLabel === "ALL" || message.labelIds.includes(activeLabel))
|
|
17
|
+
.filter((message) => activeLabel === "TRASH" || activeLabel === "SPAM" || (!message.labelIds.includes("TRASH") && !message.labelIds.includes("SPAM")))
|
|
18
|
+
.filter((message) => !query || matchesUiQuery(message, query))
|
|
19
|
+
.sort((a, b) => Number(b.internalDate) - Number(a.internalDate)), [activeLabel, query, state.messages]);
|
|
20
|
+
const selected = messages.find((message) => message.id === selectedMessageId) ?? messages[0] ?? null;
|
|
21
|
+
const selectedThread = selected ? state.messages.filter((message) => message.threadId === selected.threadId).sort((a, b) => Number(a.internalDate) - Number(b.internalDate)) : [];
|
|
22
|
+
function resetDemo() {
|
|
23
|
+
mock.reset({
|
|
24
|
+
users: demoSeed.users,
|
|
25
|
+
currentUserId: demoSeed.currentUserId,
|
|
26
|
+
labels: demoSeed.labels,
|
|
27
|
+
messages: demoSeed.messages,
|
|
28
|
+
drafts: demoSeed.drafts,
|
|
29
|
+
historyId: "1",
|
|
30
|
+
});
|
|
31
|
+
setActiveLabel("INBOX");
|
|
32
|
+
setSelectedMessageId(null);
|
|
33
|
+
setCompose(null);
|
|
34
|
+
}
|
|
35
|
+
function openDraft(message) {
|
|
36
|
+
const draft = state.drafts.find((item) => item.message.id === message.id);
|
|
37
|
+
setCompose({
|
|
38
|
+
id: draft?.id,
|
|
39
|
+
to: header(message, "To"),
|
|
40
|
+
subject: header(message, "Subject"),
|
|
41
|
+
body: bodyText(message),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function saveDraft() {
|
|
45
|
+
if (!compose)
|
|
46
|
+
return;
|
|
47
|
+
const request = { to: compose.to.split(",").map((value) => value.trim()).filter(Boolean), subject: compose.subject, body: compose.body };
|
|
48
|
+
if (compose.id) {
|
|
49
|
+
mock.updateDraft(compose.id, request);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
mock.createDraft(request);
|
|
53
|
+
}
|
|
54
|
+
setCompose(null);
|
|
55
|
+
setActiveLabel("DRAFT");
|
|
56
|
+
}
|
|
57
|
+
function sendCompose() {
|
|
58
|
+
if (!compose)
|
|
59
|
+
return;
|
|
60
|
+
if (compose.id) {
|
|
61
|
+
mock.updateDraft(compose.id, { to: compose.to.split(",").map((value) => value.trim()).filter(Boolean), subject: compose.subject, body: compose.body });
|
|
62
|
+
mock.sendDraft(compose.id);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
mock.send({ to: compose.to.split(",").map((value) => value.trim()).filter(Boolean), subject: compose.subject, body: compose.body });
|
|
66
|
+
}
|
|
67
|
+
setCompose(null);
|
|
68
|
+
setActiveLabel("SENT");
|
|
69
|
+
}
|
|
70
|
+
return (_jsxs("div", { className: "gmail-shell", children: [_jsxs("aside", { className: "gmail-sidebar", children: [_jsx("div", { className: "gmail-brand", children: "Gmail" }), _jsx("button", { className: "gmail-compose", type: "button", onClick: () => setCompose({ to: "", subject: "", body: "" }), children: "Compose" }), _jsx("nav", { children: [...primaryLabels, "ALL"].map((labelId) => {
|
|
71
|
+
const item = state.labels.find((entry) => entry.id === labelId);
|
|
72
|
+
return (_jsxs("button", { className: labelId === activeLabel ? "active" : "", type: "button", onClick: () => { setActiveLabel(labelId); setSelectedMessageId(null); }, children: [_jsx("span", { children: item?.name ?? "All Mail" }), item?.messagesUnread ? _jsx("strong", { children: item.messagesUnread }) : null] }, labelId));
|
|
73
|
+
}) }), _jsxs("section", { className: "gmail-labels", children: [_jsx("h2", { children: "Labels" }), state.labels
|
|
74
|
+
.filter((item) => item.type === "user")
|
|
75
|
+
.map((item) => (_jsx("button", { className: item.id === activeLabel ? "active" : "", type: "button", onClick: () => setActiveLabel(item.id), children: item.name }, item.id)))] }), _jsxs("section", { className: "gmail-account", children: [_jsx("strong", { children: state.users.find((user) => user.id === state.currentUserId)?.displayName }), _jsx("span", { children: state.users.find((user) => user.id === state.currentUserId)?.emailAddress }), _jsx("button", { type: "button", onClick: resetDemo, children: "Reset demo seed" })] })] }), _jsxs("main", { className: "gmail-main", children: [_jsxs("header", { className: "gmail-toolbar", children: [_jsx("input", { value: query, placeholder: "Search mail", onChange: (event) => setQuery(event.target.value) }), _jsxs("div", { children: [_jsx("strong", { children: label?.name ?? "All Mail" }), _jsxs("span", { children: [messages.length, " messages"] })] })] }), _jsxs("section", { className: "gmail-workspace", children: [_jsx("div", { className: "gmail-list", children: messages.map((message) => (_jsxs("button", { className: `${message.id === selected?.id ? "selected" : ""} ${message.labelIds.includes("UNREAD") ? "unread" : ""}`, type: "button", onClick: () => {
|
|
76
|
+
setSelectedMessageId(message.id);
|
|
77
|
+
if (message.labelIds.includes("UNREAD"))
|
|
78
|
+
mock.modifyMessage(message.id, { removeLabelIds: ["UNREAD"] });
|
|
79
|
+
}, children: [_jsx("span", { children: header(message, activeLabel === "SENT" || activeLabel === "DRAFT" ? "To" : "From") }), _jsx("strong", { children: header(message, "Subject") }), _jsx("small", { children: message.snippet }), _jsx("time", { children: formatDate(message.internalDate) })] }, message.id))) }), _jsx("article", { className: "gmail-reader", children: selected ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "gmail-reader-head", children: [_jsxs("div", { children: [_jsx("h1", { children: header(selected, "Subject") }), _jsxs("p", { children: [selectedThread.length, " message", selectedThread.length === 1 ? "" : "s", " in thread"] })] }), _jsxs("div", { className: "gmail-actions", children: [selected.labelIds.includes("DRAFT") ? _jsx("button", { type: "button", onClick: () => openDraft(selected), children: "Edit draft" }) : null, _jsx("button", { type: "button", onClick: () => mock.modifyThread(selected.threadId, { removeLabelIds: ["INBOX"] }), children: "Archive" }), _jsx("button", { type: "button", onClick: () => mock.trashThread(selected.threadId), children: "Trash" }), _jsx("button", { type: "button", onClick: () => mock.modifyThread(selected.threadId, { addLabelIds: ["STARRED"] }), children: "Star" })] })] }), _jsx("div", { className: "gmail-thread", children: selectedThread.map((message) => (_jsxs("section", { className: "gmail-thread-message", children: [_jsxs("header", { children: [_jsxs("div", { children: [_jsx("strong", { children: header(message, "From") }), _jsxs("span", { children: ["to ", header(message, "To")] })] }), _jsx("time", { children: formatDate(message.internalDate) })] }), _jsx("p", { children: bodyText(message) }), message.attachments?.length ? (_jsx("div", { className: "gmail-attachments", children: message.attachments.map((attachment) => (_jsxs("span", { children: [attachment.filename, " \u00B7 ", attachment.size, " bytes"] }, attachment.id))) })) : null] }, message.id))) })] })) : (_jsx("div", { className: "gmail-empty", children: "No messages." })) })] })] }), compose ? (_jsxs("form", { className: "gmail-composer", onSubmit: (event) => { event.preventDefault(); sendCompose(); }, children: [_jsxs("header", { children: [_jsx("strong", { children: compose.id ? "Edit draft" : "New Message" }), _jsx("button", { type: "button", onClick: () => setCompose(null), children: "\u00D7" })] }), _jsx("input", { value: compose.to, placeholder: "Recipients", onChange: (event) => setCompose({ ...compose, to: event.target.value }) }), _jsx("input", { value: compose.subject, placeholder: "Subject", onChange: (event) => setCompose({ ...compose, subject: event.target.value }) }), _jsx("textarea", { value: compose.body, onChange: (event) => setCompose({ ...compose, body: event.target.value }) }), _jsxs("footer", { children: [_jsx("button", { type: "button", onClick: saveDraft, children: "Save draft" }), _jsx("button", { className: "gmail-send", type: "submit", children: "Send" })] })] })) : null] }));
|
|
80
|
+
}
|
|
81
|
+
function header(message, name) {
|
|
82
|
+
return message.payload.headers.find((item) => item.name.toLowerCase() === name.toLowerCase())?.value ?? "";
|
|
83
|
+
}
|
|
84
|
+
function bodyText(message) {
|
|
85
|
+
return message.payload.body.data ? decodeBase64Url(message.payload.body.data) : message.snippet;
|
|
86
|
+
}
|
|
87
|
+
function matchesUiQuery(message, query) {
|
|
88
|
+
const target = [message.snippet, bodyText(message), ...message.payload.headers.map((item) => item.value)].join(" ").toLowerCase();
|
|
89
|
+
return target.includes(query.toLowerCase());
|
|
90
|
+
}
|
|
91
|
+
function formatDate(internalDate) {
|
|
92
|
+
return new Date(Number(internalDate)).toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" });
|
|
93
|
+
}
|
package/dist/ui/dev.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dev.d.ts","sourceRoot":"","sources":["../../src/ui/dev.tsx"],"names":[],"mappings":"AAIA,OAAO,cAAc,CAAC"}
|
package/dist/ui/dev.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { createGmailMock, demoSeed } from "../index";
|
|
5
|
+
import { GmailMockApp } from "./GmailMockApp";
|
|
6
|
+
import "./styles.css";
|
|
7
|
+
const mock = createGmailMock({
|
|
8
|
+
now: "2026-06-01T16:30:00.000Z",
|
|
9
|
+
seed: demoSeed,
|
|
10
|
+
});
|
|
11
|
+
createRoot(document.getElementById("root")).render(_jsx(React.StrictMode, { children: _jsx(GmailMockApp, { mock: mock }) }));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ui/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/ui/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GmailMockApp } from "./GmailMockApp";
|