imsg-mcp 1.3.2 → 1.4.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/CHANGELOG.md +25 -0
- package/README.md +3 -2
- package/dist/cli.js +47 -5
- package/dist/cli.js.map +1 -1
- package/dist/{imessage-db-Cz8MIIpG.js → imessage-db-C8WKHFQA.js} +102 -15
- package/dist/imessage-db-C8WKHFQA.js.map +1 -0
- package/dist/index.js +848 -73
- package/dist/index.js.map +1 -1
- package/dist/{meta-DzE7CSrZ.js → meta-CGz9n7A2.js} +2 -2
- package/dist/{meta-DzE7CSrZ.js.map → meta-CGz9n7A2.js.map} +1 -1
- package/dist/{setup-BuXuKAHA.js → setup-BDnz3OuH.js} +2 -2
- package/dist/{setup-BuXuKAHA.js.map → setup-BDnz3OuH.js.map} +1 -1
- package/dist/{shutdown-pPAPSnk3.js → shutdown-CqGQKFmr.js} +73 -47
- package/dist/shutdown-CqGQKFmr.js.map +1 -0
- package/dist/tui.js +216 -60
- package/dist/tui.js.map +1 -1
- package/dist/{watchdog-Cbgm050b.js → watchdog-CizaS0Jv.js} +80 -4
- package/dist/watchdog-CizaS0Jv.js.map +1 -0
- package/native/imsg-native.darwin-arm64.node +0 -0
- package/package.json +1 -1
- package/skills/humans/SKILL.md +136 -0
- package/skills/imsg-mcp/SKILL.md +8 -0
- package/dist/imessage-db-Cz8MIIpG.js.map +0 -1
- package/dist/shutdown-pPAPSnk3.js.map +0 -1
- package/dist/watchdog-Cbgm050b.js.map +0 -1
package/dist/tui.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
4
|
import { useScreenSize, withFullScreen } from "fullscreen-ink";
|
|
5
|
-
import {
|
|
5
|
+
import { b as getImsgDbPath, d as getContactsDbPaths, e as getSlugsDbPath, l as sendToChat, m as sendToChatId, n as sendMessageReliable, u as registerCleanup, c as checkLocalAccess, f as formatAccessReport, t as installShutdownHandlers, z as startHeapMonitor, x as logStartup, H as enableFileLogging, D as stopHeapMonitor, C as logShutdown } from "./shutdown-CqGQKFmr.js";
|
|
6
6
|
import { spawnSync, execSync } from "node:child_process";
|
|
7
7
|
import { resolveTuiConfig } from "./tui-config-Crn6TZPg.js";
|
|
8
|
-
import { a as rankFuzzy, b as dispatchAnalytic, I as IMPLEMENTED_TYPES,
|
|
8
|
+
import { a as rankFuzzy, b as dispatchAnalytic, I as IMPLEMENTED_TYPES, o as onMemorySample, e as readWatchdogState, m as minMessageId, r as resolveRecipient, d as defaultCountryFromEnv, i as installWatchdog } from "./watchdog-CizaS0Jv.js";
|
|
9
9
|
import { existsSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
@@ -14,7 +14,7 @@ import React, { createContext, useContext, useMemo, useEffect, useState, useRef,
|
|
|
14
14
|
import { TextInput } from "@inkjs/ui";
|
|
15
15
|
import { parseUserDate, formatJumpTarget } from "./dateParse-DJXMfq3a.js";
|
|
16
16
|
import { e as extensionFor, t as toJSON, a as toCSV, b as toMarkdown } from "./exportFormats-wytSyBTp.js";
|
|
17
|
-
import { h as hasNativeModule, I as IMessageDB } from "./imessage-db-
|
|
17
|
+
import { h as hasNativeModule, I as IMessageDB } from "./imessage-db-C8WKHFQA.js";
|
|
18
18
|
const FC_LIST_TIMEOUT_MS = 1e3;
|
|
19
19
|
let cached = null;
|
|
20
20
|
function detectNerdFont() {
|
|
@@ -807,8 +807,10 @@ const COMPOSE_NEW_KEYS = [
|
|
|
807
807
|
];
|
|
808
808
|
const FILTER_KEYS = [["Enter/Esc", "exit filter"]];
|
|
809
809
|
const DRAWER_KEYS = [
|
|
810
|
-
["j/k", "
|
|
811
|
-
["o", "open
|
|
810
|
+
["j/k", "select attachment"],
|
|
811
|
+
["o", "open"],
|
|
812
|
+
["s", "save"],
|
|
813
|
+
["y", "copy path"],
|
|
812
814
|
["Esc/q", "close"]
|
|
813
815
|
];
|
|
814
816
|
function HelpBar({ mode, focus }) {
|
|
@@ -1110,9 +1112,10 @@ function Label({ children }) {
|
|
|
1110
1112
|
": "
|
|
1111
1113
|
] }) });
|
|
1112
1114
|
}
|
|
1113
|
-
function MessageDrawer({ message: m, width, height }) {
|
|
1115
|
+
function MessageDrawer({ message: m, width, height, selectedAttachmentIdx = 0 }) {
|
|
1114
1116
|
const theme = useTheme();
|
|
1115
1117
|
const hasAttachments = m.attachments && m.attachments.length > 0;
|
|
1118
|
+
const multiAttachment = (m.attachments?.length ?? 0) > 1;
|
|
1116
1119
|
return /* @__PURE__ */ jsxs(
|
|
1117
1120
|
Box,
|
|
1118
1121
|
{
|
|
@@ -1157,6 +1160,14 @@ function MessageDrawer({ message: m, width, height }) {
|
|
|
1157
1160
|
/* @__PURE__ */ jsx(Label, { children: "GUID" }),
|
|
1158
1161
|
/* @__PURE__ */ jsx(Text, { color: theme.drawer.value, wrap: "truncate", children: m.guid })
|
|
1159
1162
|
] }),
|
|
1163
|
+
m.sendError !== void 0 && /* @__PURE__ */ jsxs(Box, { children: [
|
|
1164
|
+
/* @__PURE__ */ jsx(Label, { children: "Status" }),
|
|
1165
|
+
/* @__PURE__ */ jsxs(Text, { color: theme.edited, bold: true, children: [
|
|
1166
|
+
"NOT DELIVERED — send failed (error ",
|
|
1167
|
+
m.sendError,
|
|
1168
|
+
")"
|
|
1169
|
+
] })
|
|
1170
|
+
] }),
|
|
1160
1171
|
m.isEdited && /* @__PURE__ */ jsxs(Box, { children: [
|
|
1161
1172
|
/* @__PURE__ */ jsx(Label, { children: "Status" }),
|
|
1162
1173
|
/* @__PURE__ */ jsx(Text, { color: theme.edited, children: "Edited" })
|
|
@@ -1182,29 +1193,43 @@ function MessageDrawer({ message: m, width, height }) {
|
|
|
1182
1193
|
m.attachments.length,
|
|
1183
1194
|
"):"
|
|
1184
1195
|
] }),
|
|
1185
|
-
m.attachments.map((att) => {
|
|
1196
|
+
m.attachments.map((att, i) => {
|
|
1197
|
+
const isSel = multiAttachment && i === selectedAttachmentIdx;
|
|
1186
1198
|
return /* @__PURE__ */ jsxs(
|
|
1187
1199
|
Box,
|
|
1188
1200
|
{
|
|
1189
1201
|
flexDirection: "column",
|
|
1190
|
-
paddingLeft: 1,
|
|
1191
1202
|
children: [
|
|
1192
|
-
/* @__PURE__ */
|
|
1193
|
-
|
|
1203
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
1204
|
+
/* @__PURE__ */ jsx(Box, { flexShrink: 0, children: /* @__PURE__ */ jsx(Text, { color: isSel ? theme.sent.bg : theme.drawer.label, children: multiAttachment ? isSel ? "▸" : " " : " " }) }),
|
|
1205
|
+
/* @__PURE__ */ jsx(Text, { color: theme.drawer.value, bold: isSel, wrap: "truncate", children: att.transferName ?? att.filename })
|
|
1206
|
+
] }),
|
|
1207
|
+
/* @__PURE__ */ jsx(Box, { paddingLeft: 1, children: /* @__PURE__ */ jsxs(Text, { color: theme.drawer.label, children: [
|
|
1194
1208
|
att.mimeType ?? "unknown",
|
|
1195
1209
|
" ·",
|
|
1196
1210
|
" ",
|
|
1197
|
-
att.totalBytes > 0 ? formatBytes(att.totalBytes) : "?"
|
|
1198
|
-
|
|
1199
|
-
] })
|
|
1211
|
+
att.totalBytes > 0 ? formatBytes(att.totalBytes) : "?"
|
|
1212
|
+
] }) })
|
|
1200
1213
|
]
|
|
1201
1214
|
},
|
|
1202
1215
|
`${att.filename}-${att.transferName ?? ""}-${att.mimeType ?? ""}-${att.totalBytes}`
|
|
1203
1216
|
);
|
|
1204
1217
|
}),
|
|
1205
|
-
/* @__PURE__ */ jsxs(Box, { marginTop: 1, children: [
|
|
1206
|
-
/* @__PURE__ */
|
|
1207
|
-
|
|
1218
|
+
/* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
1219
|
+
multiAttachment && /* @__PURE__ */ jsxs(Text, { color: theme.help.desc, children: [
|
|
1220
|
+
/* @__PURE__ */ jsx(Text, { color: theme.help.key, children: "j/k" }),
|
|
1221
|
+
": select attachment"
|
|
1222
|
+
] }),
|
|
1223
|
+
/* @__PURE__ */ jsxs(Text, { color: theme.help.desc, children: [
|
|
1224
|
+
/* @__PURE__ */ jsx(Text, { color: theme.help.key, children: "o" }),
|
|
1225
|
+
": open",
|
|
1226
|
+
" ",
|
|
1227
|
+
/* @__PURE__ */ jsx(Text, { color: theme.help.key, children: "s" }),
|
|
1228
|
+
": save to ~/Downloads",
|
|
1229
|
+
" ",
|
|
1230
|
+
/* @__PURE__ */ jsx(Text, { color: theme.help.key, children: "y" }),
|
|
1231
|
+
": copy path"
|
|
1232
|
+
] })
|
|
1208
1233
|
] })
|
|
1209
1234
|
] }),
|
|
1210
1235
|
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
@@ -1527,7 +1552,8 @@ const ANALYTIC_LABEL = {
|
|
|
1527
1552
|
response_time_stats: "Response Times",
|
|
1528
1553
|
daily_heatmap: "Daily Heatmap",
|
|
1529
1554
|
tapback_summary: "Tapback Summary",
|
|
1530
|
-
year_in_review_wrapped: "Year in Review (Wrapped)"
|
|
1555
|
+
year_in_review_wrapped: "Year in Review (Wrapped)",
|
|
1556
|
+
relationship_leaderboard: "Relationship Leaderboard"
|
|
1531
1557
|
};
|
|
1532
1558
|
const RANGE_LABEL = {
|
|
1533
1559
|
"30d": "Last 30 days",
|
|
@@ -1709,8 +1735,32 @@ function AnalyticsResultView({
|
|
|
1709
1735
|
return /* @__PURE__ */ jsx(TapbacksView, { rows: data, height });
|
|
1710
1736
|
case "year_in_review_wrapped":
|
|
1711
1737
|
return /* @__PURE__ */ jsx(WrappedView, { result: data });
|
|
1738
|
+
case "relationship_leaderboard":
|
|
1739
|
+
return /* @__PURE__ */ jsx(LeaderboardView, { result: data, height });
|
|
1712
1740
|
}
|
|
1713
1741
|
}
|
|
1742
|
+
function LeaderboardView({
|
|
1743
|
+
result,
|
|
1744
|
+
height
|
|
1745
|
+
}) {
|
|
1746
|
+
const theme = useTheme();
|
|
1747
|
+
const rows = result.leaderboard.slice(0, Math.max(height - 3, 5));
|
|
1748
|
+
if (rows.length === 0) return /* @__PURE__ */ jsx(NoData, {});
|
|
1749
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: rows.map((r, i) => /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
|
|
1750
|
+
/* @__PURE__ */ jsx(Text, { color: theme.lineNum, children: String(i + 1).padStart(2) }),
|
|
1751
|
+
/* @__PURE__ */ jsx(Text, { color: theme.senderName, bold: true, children: r.contact.length > 24 ? `${r.contact.slice(0, 23)}…` : r.contact }),
|
|
1752
|
+
/* @__PURE__ */ jsxs(Text, { color: theme.info.label, children: [
|
|
1753
|
+
r.total,
|
|
1754
|
+
" msgs · ",
|
|
1755
|
+
Math.round(r.reciprocity * 100),
|
|
1756
|
+
"% recip ·",
|
|
1757
|
+
" ",
|
|
1758
|
+
Math.round(r.daysSinceLast),
|
|
1759
|
+
"d ago · score ",
|
|
1760
|
+
r.score
|
|
1761
|
+
] })
|
|
1762
|
+
] }, r.contact)) });
|
|
1763
|
+
}
|
|
1714
1764
|
function NoData() {
|
|
1715
1765
|
const theme = useTheme();
|
|
1716
1766
|
return /* @__PURE__ */ jsx(Text, { color: theme.help.desc, children: "No data in this window." });
|
|
@@ -2003,7 +2053,8 @@ const ANALYTIC_DESCRIPTION = {
|
|
|
2003
2053
|
response_time_stats: "Reply latency (median, p95, mean)",
|
|
2004
2054
|
daily_heatmap: "7×24 grid of activity by weekday × hour",
|
|
2005
2055
|
tapback_summary: "Tapback reactions sent per contact",
|
|
2006
|
-
year_in_review_wrapped: "Wrapped summary: top contacts, peak day, totals"
|
|
2056
|
+
year_in_review_wrapped: "Wrapped summary: top contacts, peak day, totals",
|
|
2057
|
+
relationship_leaderboard: "Top relationships by volume, reciprocity, and recency"
|
|
2007
2058
|
};
|
|
2008
2059
|
let instanceCounter = 0;
|
|
2009
2060
|
function makeInstance(type) {
|
|
@@ -2161,6 +2212,7 @@ function ConversationItem({
|
|
|
2161
2212
|
const truncatedName = truncateToWidth(name, nameW);
|
|
2162
2213
|
const snippetW = Math.max(width - 6, 8);
|
|
2163
2214
|
const truncatedSnippet = truncateToWidth(snippet, snippetW, "");
|
|
2215
|
+
const slugText = truncateSlugForRow(c.threadSlug ?? "", Math.max(width - 3, 10));
|
|
2164
2216
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
|
|
2165
2217
|
/* @__PURE__ */ jsxs(
|
|
2166
2218
|
Box,
|
|
@@ -2218,9 +2270,9 @@ function ConversationItem({
|
|
|
2218
2270
|
justifyContent: "flex-end",
|
|
2219
2271
|
paddingRight: 1,
|
|
2220
2272
|
backgroundColor: theme.sidebar.slugBg,
|
|
2221
|
-
children: /* @__PURE__ */ jsxs(Text, { color: theme.sidebar.slug, dimColor: true, italic: true, children: [
|
|
2273
|
+
children: /* @__PURE__ */ jsxs(Text, { color: theme.sidebar.slug, dimColor: true, italic: true, wrap: "truncate", children: [
|
|
2222
2274
|
"~",
|
|
2223
|
-
|
|
2275
|
+
slugText
|
|
2224
2276
|
] })
|
|
2225
2277
|
}
|
|
2226
2278
|
)
|
|
@@ -2230,6 +2282,16 @@ function ConversationItem({
|
|
|
2230
2282
|
!isLast && /* @__PURE__ */ jsx(Box, { paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: theme.sidebar.separator, dimColor: true, children: theme.glyphs.separator.repeat(Math.max(width - 4, 1)) }) })
|
|
2231
2283
|
] });
|
|
2232
2284
|
}
|
|
2285
|
+
function truncateSlugForRow(slug, maxCols) {
|
|
2286
|
+
if (visualWidth(slug) <= maxCols) return slug;
|
|
2287
|
+
const m = slug.match(/^(.*)(~[a-z]+~[0-9a-f]+)$/);
|
|
2288
|
+
if (m?.[1] !== void 0 && m[2] !== void 0) {
|
|
2289
|
+
const tailW = visualWidth(m[2]);
|
|
2290
|
+
const nameCols = Math.max(maxCols - tailW, 3);
|
|
2291
|
+
return `${truncateToWidth(m[1], nameCols)}${m[2]}`;
|
|
2292
|
+
}
|
|
2293
|
+
return truncateToWidth(slug, maxCols);
|
|
2294
|
+
}
|
|
2233
2295
|
function DefaultModuleSidebarItem({
|
|
2234
2296
|
instance,
|
|
2235
2297
|
selected,
|
|
@@ -2514,13 +2576,17 @@ function MessageBubble({
|
|
|
2514
2576
|
] })
|
|
2515
2577
|
)
|
|
2516
2578
|
] }) }),
|
|
2517
|
-
/* @__PURE__ */ jsx(Text, { color: isSent ? theme.sentText : theme.receivedText, wrap: "
|
|
2579
|
+
/* @__PURE__ */ jsx(Text, { color: isSent ? theme.sentText : theme.receivedText, wrap: "wrap", children: text }),
|
|
2518
2580
|
reactions && /* @__PURE__ */ jsxs(Text, { children: [
|
|
2519
2581
|
" ",
|
|
2520
2582
|
reactions
|
|
2521
2583
|
] }),
|
|
2522
2584
|
hasAttachments && /* @__PURE__ */ jsx(Text, { color: theme.attachment, children: " 📎" }),
|
|
2523
|
-
m.isEdited && /* @__PURE__ */ jsx(Text, { color: theme.edited, children: " ✎" })
|
|
2585
|
+
m.isEdited && /* @__PURE__ */ jsx(Text, { color: theme.edited, children: " ✎" }),
|
|
2586
|
+
m.sendError !== void 0 && /* @__PURE__ */ jsxs(Text, { color: theme.edited, bold: true, children: [
|
|
2587
|
+
" ",
|
|
2588
|
+
"✗ not delivered"
|
|
2589
|
+
] })
|
|
2524
2590
|
] }),
|
|
2525
2591
|
m.isReply && (() => {
|
|
2526
2592
|
let replyText = m.replyTo?.replyToText ?? null;
|
|
@@ -2531,7 +2597,7 @@ function MessageBubble({
|
|
|
2531
2597
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
2532
2598
|
lineNum !== void 0 && /* @__PURE__ */ jsx(Text, { children: " " }),
|
|
2533
2599
|
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
2534
|
-
/* @__PURE__ */ jsxs(Text, { color: theme.replyContext, italic: true, children: [
|
|
2600
|
+
/* @__PURE__ */ jsxs(Text, { color: theme.replyContext, italic: true, wrap: "truncate", children: [
|
|
2535
2601
|
" ↩ ",
|
|
2536
2602
|
display
|
|
2537
2603
|
] })
|
|
@@ -2553,7 +2619,7 @@ function PendingBubble({ text, status }) {
|
|
|
2553
2619
|
] }),
|
|
2554
2620
|
/* @__PURE__ */ jsx(Text, { color: theme.timestamp, children: "now".padEnd(14) }),
|
|
2555
2621
|
/* @__PURE__ */ jsx(Text, { color: theme.sent.bg, bold: true, children: `${theme.glyphs.sent} Me: ` }),
|
|
2556
|
-
/* @__PURE__ */ jsx(Text, { color: theme.pending.fg, wrap: "
|
|
2622
|
+
/* @__PURE__ */ jsx(Text, { color: theme.pending.fg, wrap: "wrap", children: text })
|
|
2557
2623
|
] });
|
|
2558
2624
|
}
|
|
2559
2625
|
function ThreadPane({
|
|
@@ -2589,10 +2655,10 @@ function ThreadPane({
|
|
|
2589
2655
|
if (cursorIdx >= messages.length - NEAR_END && pending.length === 0) {
|
|
2590
2656
|
const end2 = messages.length - 1;
|
|
2591
2657
|
let start2 = end2;
|
|
2592
|
-
let totalLines2 = lineHeight(messages, end2, maxBubbleW);
|
|
2593
|
-
while (start2 > 0 && totalLines2 + lineHeight(messages, start2 - 1, maxBubbleW) <= msgAreaHeight) {
|
|
2658
|
+
let totalLines2 = lineHeight(messages, end2, maxBubbleW, isGroup);
|
|
2659
|
+
while (start2 > 0 && totalLines2 + lineHeight(messages, start2 - 1, maxBubbleW, isGroup) <= msgAreaHeight) {
|
|
2594
2660
|
start2--;
|
|
2595
|
-
totalLines2 += lineHeight(messages, start2, maxBubbleW);
|
|
2661
|
+
totalLines2 += lineHeight(messages, start2, maxBubbleW, isGroup);
|
|
2596
2662
|
}
|
|
2597
2663
|
return { visibleStart: start2, visibleEnd: end2 + 1 };
|
|
2598
2664
|
}
|
|
@@ -2601,24 +2667,24 @@ function ThreadPane({
|
|
|
2601
2667
|
const targetAbove = Math.floor(msgAreaHeight * 0.4);
|
|
2602
2668
|
while (start > 0 && linesAbove < targetAbove) {
|
|
2603
2669
|
start--;
|
|
2604
|
-
linesAbove += lineHeight(messages, start, maxBubbleW);
|
|
2670
|
+
linesAbove += lineHeight(messages, start, maxBubbleW, isGroup);
|
|
2605
2671
|
}
|
|
2606
2672
|
let end = cursorIdx;
|
|
2607
|
-
let totalLines = linesAbove + lineHeight(messages, cursorIdx, maxBubbleW);
|
|
2673
|
+
let totalLines = linesAbove + lineHeight(messages, cursorIdx, maxBubbleW, isGroup);
|
|
2608
2674
|
while (end < total - 1 && totalLines < msgAreaHeight) {
|
|
2609
2675
|
end++;
|
|
2610
2676
|
if (end < messages.length) {
|
|
2611
|
-
totalLines += lineHeight(messages, end, maxBubbleW);
|
|
2677
|
+
totalLines += lineHeight(messages, end, maxBubbleW, isGroup);
|
|
2612
2678
|
} else {
|
|
2613
2679
|
totalLines += 1;
|
|
2614
2680
|
}
|
|
2615
2681
|
}
|
|
2616
2682
|
while (start > 0 && totalLines < msgAreaHeight) {
|
|
2617
2683
|
start--;
|
|
2618
|
-
totalLines += lineHeight(messages, start, maxBubbleW);
|
|
2684
|
+
totalLines += lineHeight(messages, start, maxBubbleW, isGroup);
|
|
2619
2685
|
}
|
|
2620
2686
|
return { visibleStart: start, visibleEnd: end + 1 };
|
|
2621
|
-
}, [messages, pending.length, selectedMsgIdx, msgAreaHeight, maxBubbleW]);
|
|
2687
|
+
}, [messages, pending.length, selectedMsgIdx, msgAreaHeight, maxBubbleW, isGroup]);
|
|
2622
2688
|
const visibleMessages = messages.slice(visibleStart, Math.min(visibleEnd, messages.length));
|
|
2623
2689
|
const messagesByGuid = useMemo(() => {
|
|
2624
2690
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -2748,22 +2814,27 @@ function ThreadPane({
|
|
|
2748
2814
|
}
|
|
2749
2815
|
);
|
|
2750
2816
|
}
|
|
2751
|
-
function
|
|
2817
|
+
function wrapRows(text, innerW) {
|
|
2818
|
+
let rows = 0;
|
|
2819
|
+
for (const line of text.split("\n")) {
|
|
2820
|
+
rows += Math.max(1, Math.ceil(visualWidth(line) / innerW));
|
|
2821
|
+
}
|
|
2822
|
+
return rows;
|
|
2823
|
+
}
|
|
2824
|
+
function lineHeight(messages, i, bubbleWidth, isGroup) {
|
|
2752
2825
|
if (i < 0 || i >= messages.length) return 1;
|
|
2753
2826
|
const msg = messages[i];
|
|
2754
|
-
const
|
|
2827
|
+
const firstInGroup = isGroupStart(msg, i > 0 ? messages[i - 1] : void 0);
|
|
2828
|
+
const senderPad = isGroup && firstInGroup && !msg.isFromMe ? 14 : 0;
|
|
2829
|
+
const innerW = Math.max(16, bubbleWidth - 25 - senderPad);
|
|
2755
2830
|
const text = msg.text ?? "";
|
|
2756
|
-
let h =
|
|
2757
|
-
if (msg.isReply)
|
|
2758
|
-
const replyLen = msg.replyTo?.replyToText?.length ?? 0;
|
|
2759
|
-
h += Math.max(1, Math.ceil(replyLen / Math.max(20, innerW - 4)));
|
|
2760
|
-
}
|
|
2831
|
+
let h = wrapRows(text, innerW);
|
|
2832
|
+
if (msg.isReply) h += 1;
|
|
2761
2833
|
if (i === 0) {
|
|
2762
2834
|
h += 1;
|
|
2763
2835
|
} else if (isDifferentDay(messages[i - 1].date, msg.date)) {
|
|
2764
2836
|
h += 2;
|
|
2765
2837
|
}
|
|
2766
|
-
if (msg.attachments && msg.attachments.length > 0) h += 1;
|
|
2767
2838
|
return h;
|
|
2768
2839
|
}
|
|
2769
2840
|
function nextGroupBoundary(messages, fromIdx) {
|
|
@@ -2836,7 +2907,6 @@ function useDevStats(visible) {
|
|
|
2836
2907
|
const lastTimeRef = useRef(Date.now());
|
|
2837
2908
|
const lastQueryMsRef = useRef(null);
|
|
2838
2909
|
useEffect(() => {
|
|
2839
|
-
if (!visible) return;
|
|
2840
2910
|
const sample = () => {
|
|
2841
2911
|
const now = Date.now();
|
|
2842
2912
|
const elapsed = now - lastTimeRef.current;
|
|
@@ -2863,9 +2933,13 @@ function useDevStats(visible) {
|
|
|
2863
2933
|
});
|
|
2864
2934
|
};
|
|
2865
2935
|
sample();
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2936
|
+
if (visible) {
|
|
2937
|
+
const timer = setInterval(sample, 2e3);
|
|
2938
|
+
timer.unref();
|
|
2939
|
+
return () => clearInterval(timer);
|
|
2940
|
+
}
|
|
2941
|
+
const unsubscribe = onMemorySample(() => sample());
|
|
2942
|
+
return unsubscribe;
|
|
2869
2943
|
}, [visible]);
|
|
2870
2944
|
const recordQueryTime = useCallback((ms) => {
|
|
2871
2945
|
lastQueryMsRef.current = Math.round(ms * 10) / 10;
|
|
@@ -3027,7 +3101,8 @@ function useImsg() {
|
|
|
3027
3101
|
if (slugRecord.isGroup) {
|
|
3028
3102
|
return slugRecord.displayName && !slugRecord.displayName.startsWith("chat") ? sendToChat(slugRecord.displayName, text) : sendToChatId(slugRecord.chatGuid, text);
|
|
3029
3103
|
}
|
|
3030
|
-
|
|
3104
|
+
const preferred = db.getPreferredSendService(slugRecord.chatIdentifier) ?? (slugRecord.service === "SMS" ? "SMS" : "iMessage");
|
|
3105
|
+
return sendMessageReliable(slugRecord.chatIdentifier, text, preferred);
|
|
3031
3106
|
},
|
|
3032
3107
|
[getDb]
|
|
3033
3108
|
);
|
|
@@ -3051,9 +3126,12 @@ function useImsg() {
|
|
|
3051
3126
|
error: `Ambiguous: ${resolution.candidates.length} matches for "${input}". Pick a specific handle.`
|
|
3052
3127
|
};
|
|
3053
3128
|
}
|
|
3054
|
-
|
|
3129
|
+
const db = getDb();
|
|
3130
|
+
const conv = await db.findChatByHandle(resolution.handle);
|
|
3131
|
+
const preferred = conv && !conv.isGroupChat ? db.getPreferredSendService(conv.chatIdentifier) ?? (conv.serviceType === "SMS" ? "SMS" : "iMessage") : void 0;
|
|
3132
|
+
return sendMessageReliable(resolution.handle, text, preferred);
|
|
3055
3133
|
},
|
|
3056
|
-
[resolveRecipientInput]
|
|
3134
|
+
[resolveRecipientInput, getDb]
|
|
3057
3135
|
);
|
|
3058
3136
|
const refresh = useCallback(() => {
|
|
3059
3137
|
getDb().scheduleBackgroundRefresh();
|
|
@@ -3124,6 +3202,7 @@ const initialState = {
|
|
|
3124
3202
|
status: "Loading...",
|
|
3125
3203
|
numBuffer: "",
|
|
3126
3204
|
showDevStats: false,
|
|
3205
|
+
drawerAttachmentIdx: 0,
|
|
3127
3206
|
conversationLoadedCount: 0,
|
|
3128
3207
|
conversationLoadingMore: false,
|
|
3129
3208
|
messageOldestLoadedId: null,
|
|
@@ -3382,9 +3461,11 @@ function reducer(state, action) {
|
|
|
3382
3461
|
case "EXIT_FILTER":
|
|
3383
3462
|
return { ...state, mode: "browse", filterQuery: "" };
|
|
3384
3463
|
case "OPEN_DRAWER":
|
|
3385
|
-
return { ...state, mode: "drawer" };
|
|
3464
|
+
return { ...state, mode: "drawer", drawerAttachmentIdx: 0 };
|
|
3386
3465
|
case "CLOSE_DRAWER":
|
|
3387
3466
|
return { ...state, mode: "browse" };
|
|
3467
|
+
case "SET_DRAWER_ATTACHMENT":
|
|
3468
|
+
return { ...state, drawerAttachmentIdx: Math.max(0, action.index) };
|
|
3388
3469
|
case "SET_NUM_BUFFER":
|
|
3389
3470
|
return { ...state, numBuffer: action.value };
|
|
3390
3471
|
case "TOGGLE_DEV_STATS":
|
|
@@ -3476,8 +3557,8 @@ function App() {
|
|
|
3476
3557
|
const commands2 = useMemo(() => allCommands(state), [state]);
|
|
3477
3558
|
const commandCtx = useMemo(() => ({ state, dispatch, imsg }), [state, imsg]);
|
|
3478
3559
|
const loadMessages = useCallback(
|
|
3479
|
-
async (idx) => {
|
|
3480
|
-
const conv = state.conversations[idx];
|
|
3560
|
+
async (idx, convOverride) => {
|
|
3561
|
+
const conv = convOverride ?? state.conversations[idx];
|
|
3481
3562
|
if (!conv) return;
|
|
3482
3563
|
dispatch({
|
|
3483
3564
|
type: "SET_LOADING",
|
|
@@ -3498,11 +3579,15 @@ function App() {
|
|
|
3498
3579
|
dispatch({ type: "SET_CONVERSATIONS", data: convs });
|
|
3499
3580
|
if (convs.length > 0) {
|
|
3500
3581
|
const prevSlug = selected?.threadSlug;
|
|
3582
|
+
let targetIdx = Math.min(Math.max(state.selectedIdx, 0), convs.length - 1);
|
|
3501
3583
|
if (prevSlug) {
|
|
3502
3584
|
const idx = convs.findIndex((c) => c.threadSlug === prevSlug);
|
|
3503
|
-
if (idx >= 0)
|
|
3585
|
+
if (idx >= 0) {
|
|
3586
|
+
dispatch({ type: "SELECT", index: idx, visibleCount: sidebarVisibleCount });
|
|
3587
|
+
targetIdx = idx;
|
|
3588
|
+
}
|
|
3504
3589
|
}
|
|
3505
|
-
await loadMessages(
|
|
3590
|
+
await loadMessages(targetIdx, convs[targetIdx]);
|
|
3506
3591
|
}
|
|
3507
3592
|
imsg.refresh();
|
|
3508
3593
|
dispatch({ type: "SET_LOADING", loading: false, status: "" });
|
|
@@ -3590,11 +3675,19 @@ function App() {
|
|
|
3590
3675
|
try {
|
|
3591
3676
|
const msgs = await imsg.loadMessages(selected.chatIdentifier);
|
|
3592
3677
|
const found = msgs.some((m) => m.isFromMe && m.text?.includes(text));
|
|
3593
|
-
|
|
3678
|
+
const failed = msgs.some(
|
|
3679
|
+
(m) => m.isFromMe && m.sendError !== void 0 && m.text?.includes(text)
|
|
3680
|
+
);
|
|
3681
|
+
if (failed) {
|
|
3682
|
+
dispatch({ type: "SET_MESSAGES", data: msgs });
|
|
3683
|
+
dispatch({ type: "FAIL_PENDING", text });
|
|
3684
|
+
} else if (found) {
|
|
3594
3685
|
dispatch({ type: "SET_MESSAGES", data: msgs });
|
|
3595
3686
|
dispatch({ type: "RESOLVE_PENDING", text });
|
|
3596
3687
|
} else if (attempt < 7) {
|
|
3597
3688
|
pollTimerRef.current = setTimeout(poll, 1500);
|
|
3689
|
+
} else {
|
|
3690
|
+
dispatch({ type: "FAIL_PENDING", text });
|
|
3598
3691
|
}
|
|
3599
3692
|
} catch {
|
|
3600
3693
|
dispatch({ type: "FAIL_PENDING", text });
|
|
@@ -3690,10 +3783,29 @@ function App() {
|
|
|
3690
3783
|
return;
|
|
3691
3784
|
}
|
|
3692
3785
|
if (state.mode === "drawer") {
|
|
3786
|
+
const attCount = selectedMsg?.attachments?.length ?? 0;
|
|
3693
3787
|
if (key.escape || input === "q") {
|
|
3694
3788
|
dispatch({ type: "CLOSE_DRAWER" });
|
|
3789
|
+
} else if ((input === "j" || key.downArrow) && attCount > 1) {
|
|
3790
|
+
dispatch({
|
|
3791
|
+
type: "SET_DRAWER_ATTACHMENT",
|
|
3792
|
+
index: Math.min(state.drawerAttachmentIdx + 1, attCount - 1)
|
|
3793
|
+
});
|
|
3794
|
+
} else if ((input === "k" || key.upArrow) && attCount > 1) {
|
|
3795
|
+
dispatch({ type: "SET_DRAWER_ATTACHMENT", index: state.drawerAttachmentIdx - 1 });
|
|
3695
3796
|
} else if (input === "o" && selectedMsg) {
|
|
3696
|
-
openAttachment(selectedMsg);
|
|
3797
|
+
openAttachment(selectedMsg, state.drawerAttachmentIdx);
|
|
3798
|
+
} else if (input === "s" && selectedMsg) {
|
|
3799
|
+
saveAttachment(selectedMsg, state.drawerAttachmentIdx);
|
|
3800
|
+
} else if (input === "y" && selectedMsg) {
|
|
3801
|
+
const att = selectedMsg.attachments?.[state.drawerAttachmentIdx];
|
|
3802
|
+
if (att?.filename) {
|
|
3803
|
+
const filepath = att.filename.replace(/^~/, process.env.HOME ?? "~");
|
|
3804
|
+
execSync("pbcopy", { input: filepath });
|
|
3805
|
+
dispatch({ type: "SET_STATUS", status: "Attachment path copied." });
|
|
3806
|
+
} else {
|
|
3807
|
+
dispatch({ type: "SET_STATUS", status: "No attachment to copy." });
|
|
3808
|
+
}
|
|
3697
3809
|
}
|
|
3698
3810
|
return;
|
|
3699
3811
|
}
|
|
@@ -3938,9 +4050,16 @@ function App() {
|
|
|
3938
4050
|
const prev = prevGroupBoundary(state.messages, state.selectedMsgIdx);
|
|
3939
4051
|
dispatch({ type: "SELECT_MSG", index: prev });
|
|
3940
4052
|
} else if (input === "o") {
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
4053
|
+
const msg = state.selectedMsgIdx >= 0 ? state.messages[state.selectedMsgIdx] : void 0;
|
|
4054
|
+
if (msg && (msg.attachments?.length ?? 0) > 1) {
|
|
4055
|
+
dispatch({ type: "OPEN_DRAWER" });
|
|
4056
|
+
dispatch({
|
|
4057
|
+
type: "SET_STATUS",
|
|
4058
|
+
status: `${msg.attachments?.length} attachments — j/k select, o open, s save`
|
|
4059
|
+
});
|
|
4060
|
+
} else {
|
|
4061
|
+
openAttachment(msg);
|
|
4062
|
+
}
|
|
3944
4063
|
}
|
|
3945
4064
|
}
|
|
3946
4065
|
});
|
|
@@ -4037,7 +4156,7 @@ function App() {
|
|
|
4037
4156
|
state.selectionAnchor,
|
|
4038
4157
|
selected
|
|
4039
4158
|
]);
|
|
4040
|
-
function openAttachment(msg) {
|
|
4159
|
+
function openAttachment(msg, attIdx = 0) {
|
|
4041
4160
|
if (!msg) {
|
|
4042
4161
|
dispatch({ type: "SET_STATUS", status: "No message selected." });
|
|
4043
4162
|
return;
|
|
@@ -4046,7 +4165,7 @@ function App() {
|
|
|
4046
4165
|
dispatch({ type: "SET_STATUS", status: "No attachment on this message." });
|
|
4047
4166
|
return;
|
|
4048
4167
|
}
|
|
4049
|
-
const att = msg.attachments[0];
|
|
4168
|
+
const att = msg.attachments[attIdx] ?? msg.attachments[0];
|
|
4050
4169
|
if (!att.filename) {
|
|
4051
4170
|
dispatch({ type: "SET_STATUS", status: "Attachment has no file path." });
|
|
4052
4171
|
return;
|
|
@@ -4064,6 +4183,35 @@ function App() {
|
|
|
4064
4183
|
}
|
|
4065
4184
|
});
|
|
4066
4185
|
}
|
|
4186
|
+
function saveAttachment(msg, attIdx) {
|
|
4187
|
+
const att = msg.attachments?.[attIdx];
|
|
4188
|
+
if (!att?.filename) {
|
|
4189
|
+
dispatch({ type: "SET_STATUS", status: "No attachment to save." });
|
|
4190
|
+
return;
|
|
4191
|
+
}
|
|
4192
|
+
const src = att.filename.replace(/^~/, process.env.HOME ?? "~");
|
|
4193
|
+
import("node:fs").then((fs) => {
|
|
4194
|
+
import("node:path").then((path) => {
|
|
4195
|
+
try {
|
|
4196
|
+
const base = att.transferName ?? path.basename(src);
|
|
4197
|
+
const dir = join(homedir(), "Downloads");
|
|
4198
|
+
const ext = path.extname(base);
|
|
4199
|
+
const stem = base.slice(0, base.length - ext.length);
|
|
4200
|
+
let dest = path.join(dir, base);
|
|
4201
|
+
for (let n = 1; fs.existsSync(dest); n++) {
|
|
4202
|
+
dest = path.join(dir, `${stem}-${n}${ext}`);
|
|
4203
|
+
}
|
|
4204
|
+
fs.copyFileSync(src, dest);
|
|
4205
|
+
dispatch({ type: "SET_STATUS", status: `Saved to ${dest.replace(homedir(), "~")}` });
|
|
4206
|
+
} catch (e) {
|
|
4207
|
+
dispatch({
|
|
4208
|
+
type: "SET_STATUS",
|
|
4209
|
+
status: `Save failed: ${e instanceof Error ? e.message : String(e)}`
|
|
4210
|
+
});
|
|
4211
|
+
}
|
|
4212
|
+
});
|
|
4213
|
+
});
|
|
4214
|
+
}
|
|
4067
4215
|
const handleMouse = useCallback(
|
|
4068
4216
|
(event) => {
|
|
4069
4217
|
if (event.type === "click") {
|
|
@@ -4163,7 +4311,15 @@ function App() {
|
|
|
4163
4311
|
onSubmitCompose: (text) => text.trim() && dispatch({ type: "CONFIRM_SEND" })
|
|
4164
4312
|
}
|
|
4165
4313
|
),
|
|
4166
|
-
state.mode === "drawer" && selectedMsg && /* @__PURE__ */ jsx(
|
|
4314
|
+
state.mode === "drawer" && selectedMsg && /* @__PURE__ */ jsx(
|
|
4315
|
+
MessageDrawer,
|
|
4316
|
+
{
|
|
4317
|
+
message: selectedMsg,
|
|
4318
|
+
width: drawerWidth,
|
|
4319
|
+
height: bodyHeight,
|
|
4320
|
+
selectedAttachmentIdx: state.drawerAttachmentIdx
|
|
4321
|
+
}
|
|
4322
|
+
),
|
|
4167
4323
|
state.showDevStats && /* @__PURE__ */ jsx(DevStats, { stats: devStats, width: devStatsWidth })
|
|
4168
4324
|
] }),
|
|
4169
4325
|
state.mode === "date-jump" && /* @__PURE__ */ jsx(
|