imsg-mcp 1.3.2 → 1.5.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 +44 -0
- package/README.md +3 -2
- package/dist/cli.js +126 -20
- package/dist/cli.js.map +1 -1
- package/dist/{imessage-db-Cz8MIIpG.js → imessage-db-BrHJPOq7.js} +147 -19
- package/dist/imessage-db-BrHJPOq7.js.map +1 -0
- package/dist/index.js +847 -73
- package/dist/index.js.map +1 -1
- package/dist/meta-E4VgVmPs.js +165 -0
- package/dist/meta-E4VgVmPs.js.map +1 -0
- package/dist/{setup-BuXuKAHA.js → setup-CysR5gId.js} +2 -2
- package/dist/{setup-BuXuKAHA.js.map → setup-CysR5gId.js.map} +1 -1
- package/dist/{shutdown-pPAPSnk3.js → shutdown-Vh0gjb6m.js} +434 -48
- package/dist/shutdown-Vh0gjb6m.js.map +1 -0
- package/dist/tui.js +226 -64
- package/dist/tui.js.map +1 -1
- package/dist/{watchdog-Cbgm050b.js → watchdog-B3T-dQT-.js} +21 -266
- package/dist/watchdog-B3T-dQT-.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 +29 -0
- package/dist/imessage-db-Cz8MIIpG.js.map +0 -1
- package/dist/meta-DzE7CSrZ.js +0 -7
- package/dist/meta-DzE7CSrZ.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 { u as dispatchAnalytic, I as IMPLEMENTED_TYPES, b as getImsgDbPath, d as getContactsDbPaths, e as getSlugsDbPath, l as sendToChat, m as sendToChatId, n as sendMessageReliable, w as registerCleanup, c as checkLocalAccess, f as formatAccessReport, v as installShutdownHandlers, B as startHeapMonitor, z as logStartup, L as enableFileLogging, F as stopHeapMonitor, E as logShutdown } from "./shutdown-Vh0gjb6m.js";
|
|
6
6
|
import { spawnSync, execSync } from "node:child_process";
|
|
7
7
|
import { resolveTuiConfig } from "./tui-config-Crn6TZPg.js";
|
|
8
|
-
import { a as rankFuzzy,
|
|
8
|
+
import { a as rankFuzzy, o as onMemorySample, b as readWatchdogState, m as minMessageId, r as resolveRecipient, d as defaultCountryFromEnv, i as installWatchdog } from "./watchdog-B3T-dQT-.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-BrHJPOq7.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,34 @@ 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
|
+
const max = rows[0]?.score || 0;
|
|
1750
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: rows.map((r, i) => /* @__PURE__ */ jsxs(Box, { gap: 1, children: [
|
|
1751
|
+
/* @__PURE__ */ jsx(Text, { color: theme.lineNum, children: String(i + 1).padStart(2) }),
|
|
1752
|
+
/* @__PURE__ */ jsx(Text, { color: theme.senderName, bold: true, children: r.contact.length > 24 ? `${r.contact.slice(0, 23)}…` : r.contact }),
|
|
1753
|
+
/* @__PURE__ */ jsxs(Text, { color: theme.info.label, children: [
|
|
1754
|
+
r.total,
|
|
1755
|
+
" msgs · ",
|
|
1756
|
+
Math.round(r.reciprocity * 100),
|
|
1757
|
+
"% recip ·",
|
|
1758
|
+
" ",
|
|
1759
|
+
Math.round(r.daysSinceLast),
|
|
1760
|
+
"d ago · score",
|
|
1761
|
+
" ",
|
|
1762
|
+
max > 0 ? Math.round(r.score / max * 100) : 0
|
|
1763
|
+
] })
|
|
1764
|
+
] }, r.contact)) });
|
|
1765
|
+
}
|
|
1714
1766
|
function NoData() {
|
|
1715
1767
|
const theme = useTheme();
|
|
1716
1768
|
return /* @__PURE__ */ jsx(Text, { color: theme.help.desc, children: "No data in this window." });
|
|
@@ -2003,17 +2055,19 @@ const ANALYTIC_DESCRIPTION = {
|
|
|
2003
2055
|
response_time_stats: "Reply latency (median, p95, mean)",
|
|
2004
2056
|
daily_heatmap: "7×24 grid of activity by weekday × hour",
|
|
2005
2057
|
tapback_summary: "Tapback reactions sent per contact",
|
|
2006
|
-
year_in_review_wrapped: "Wrapped summary: top contacts, peak day, totals"
|
|
2058
|
+
year_in_review_wrapped: "Wrapped summary: top contacts, peak day, totals",
|
|
2059
|
+
relationship_leaderboard: "Top relationships by volume, reciprocity, and recency"
|
|
2007
2060
|
};
|
|
2008
2061
|
let instanceCounter = 0;
|
|
2009
2062
|
function makeInstance(type) {
|
|
2010
2063
|
instanceCounter += 1;
|
|
2011
|
-
const
|
|
2064
|
+
const range = type === "relationship_leaderboard" ? "all" : "30d";
|
|
2065
|
+
const state = { type, range, chatIdentifier: null };
|
|
2012
2066
|
return {
|
|
2013
2067
|
id: `analytics.${type}-${Date.now()}-${instanceCounter}`,
|
|
2014
2068
|
moduleId: "analytics",
|
|
2015
2069
|
title: `Analytics: ${ANALYTIC_LABEL[type]}`,
|
|
2016
|
-
subtitle:
|
|
2070
|
+
subtitle: `range: ${range}`,
|
|
2017
2071
|
state
|
|
2018
2072
|
};
|
|
2019
2073
|
}
|
|
@@ -2161,6 +2215,7 @@ function ConversationItem({
|
|
|
2161
2215
|
const truncatedName = truncateToWidth(name, nameW);
|
|
2162
2216
|
const snippetW = Math.max(width - 6, 8);
|
|
2163
2217
|
const truncatedSnippet = truncateToWidth(snippet, snippetW, "");
|
|
2218
|
+
const slugText = truncateSlugForRow(c.threadSlug ?? "", Math.max(width - 3, 10));
|
|
2164
2219
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width, children: [
|
|
2165
2220
|
/* @__PURE__ */ jsxs(
|
|
2166
2221
|
Box,
|
|
@@ -2218,9 +2273,9 @@ function ConversationItem({
|
|
|
2218
2273
|
justifyContent: "flex-end",
|
|
2219
2274
|
paddingRight: 1,
|
|
2220
2275
|
backgroundColor: theme.sidebar.slugBg,
|
|
2221
|
-
children: /* @__PURE__ */ jsxs(Text, { color: theme.sidebar.slug, dimColor: true, italic: true, children: [
|
|
2276
|
+
children: /* @__PURE__ */ jsxs(Text, { color: theme.sidebar.slug, dimColor: true, italic: true, wrap: "truncate", children: [
|
|
2222
2277
|
"~",
|
|
2223
|
-
|
|
2278
|
+
slugText
|
|
2224
2279
|
] })
|
|
2225
2280
|
}
|
|
2226
2281
|
)
|
|
@@ -2230,6 +2285,16 @@ function ConversationItem({
|
|
|
2230
2285
|
!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
2286
|
] });
|
|
2232
2287
|
}
|
|
2288
|
+
function truncateSlugForRow(slug, maxCols) {
|
|
2289
|
+
if (visualWidth(slug) <= maxCols) return slug;
|
|
2290
|
+
const m = slug.match(/^(.*)(~[a-z]+~[0-9a-f]+)$/);
|
|
2291
|
+
if (m?.[1] !== void 0 && m[2] !== void 0) {
|
|
2292
|
+
const tailW = visualWidth(m[2]);
|
|
2293
|
+
const nameCols = Math.max(maxCols - tailW, 3);
|
|
2294
|
+
return `${truncateToWidth(m[1], nameCols)}${m[2]}`;
|
|
2295
|
+
}
|
|
2296
|
+
return truncateToWidth(slug, maxCols);
|
|
2297
|
+
}
|
|
2233
2298
|
function DefaultModuleSidebarItem({
|
|
2234
2299
|
instance,
|
|
2235
2300
|
selected,
|
|
@@ -2514,13 +2579,17 @@ function MessageBubble({
|
|
|
2514
2579
|
] })
|
|
2515
2580
|
)
|
|
2516
2581
|
] }) }),
|
|
2517
|
-
/* @__PURE__ */ jsx(Text, { color: isSent ? theme.sentText : theme.receivedText, wrap: "
|
|
2582
|
+
/* @__PURE__ */ jsx(Text, { color: isSent ? theme.sentText : theme.receivedText, wrap: "wrap", children: text }),
|
|
2518
2583
|
reactions && /* @__PURE__ */ jsxs(Text, { children: [
|
|
2519
2584
|
" ",
|
|
2520
2585
|
reactions
|
|
2521
2586
|
] }),
|
|
2522
2587
|
hasAttachments && /* @__PURE__ */ jsx(Text, { color: theme.attachment, children: " 📎" }),
|
|
2523
|
-
m.isEdited && /* @__PURE__ */ jsx(Text, { color: theme.edited, children: " ✎" })
|
|
2588
|
+
m.isEdited && /* @__PURE__ */ jsx(Text, { color: theme.edited, children: " ✎" }),
|
|
2589
|
+
m.sendError !== void 0 && /* @__PURE__ */ jsxs(Text, { color: theme.edited, bold: true, children: [
|
|
2590
|
+
" ",
|
|
2591
|
+
"✗ not delivered"
|
|
2592
|
+
] })
|
|
2524
2593
|
] }),
|
|
2525
2594
|
m.isReply && (() => {
|
|
2526
2595
|
let replyText = m.replyTo?.replyToText ?? null;
|
|
@@ -2531,7 +2600,7 @@ function MessageBubble({
|
|
|
2531
2600
|
return /* @__PURE__ */ jsxs(Box, { children: [
|
|
2532
2601
|
lineNum !== void 0 && /* @__PURE__ */ jsx(Text, { children: " " }),
|
|
2533
2602
|
/* @__PURE__ */ jsx(Text, { children: " " }),
|
|
2534
|
-
/* @__PURE__ */ jsxs(Text, { color: theme.replyContext, italic: true, children: [
|
|
2603
|
+
/* @__PURE__ */ jsxs(Text, { color: theme.replyContext, italic: true, wrap: "truncate", children: [
|
|
2535
2604
|
" ↩ ",
|
|
2536
2605
|
display
|
|
2537
2606
|
] })
|
|
@@ -2553,7 +2622,7 @@ function PendingBubble({ text, status }) {
|
|
|
2553
2622
|
] }),
|
|
2554
2623
|
/* @__PURE__ */ jsx(Text, { color: theme.timestamp, children: "now".padEnd(14) }),
|
|
2555
2624
|
/* @__PURE__ */ jsx(Text, { color: theme.sent.bg, bold: true, children: `${theme.glyphs.sent} Me: ` }),
|
|
2556
|
-
/* @__PURE__ */ jsx(Text, { color: theme.pending.fg, wrap: "
|
|
2625
|
+
/* @__PURE__ */ jsx(Text, { color: theme.pending.fg, wrap: "wrap", children: text })
|
|
2557
2626
|
] });
|
|
2558
2627
|
}
|
|
2559
2628
|
function ThreadPane({
|
|
@@ -2589,10 +2658,10 @@ function ThreadPane({
|
|
|
2589
2658
|
if (cursorIdx >= messages.length - NEAR_END && pending.length === 0) {
|
|
2590
2659
|
const end2 = messages.length - 1;
|
|
2591
2660
|
let start2 = end2;
|
|
2592
|
-
let totalLines2 = lineHeight(messages, end2, maxBubbleW);
|
|
2593
|
-
while (start2 > 0 && totalLines2 + lineHeight(messages, start2 - 1, maxBubbleW) <= msgAreaHeight) {
|
|
2661
|
+
let totalLines2 = lineHeight(messages, end2, maxBubbleW, isGroup);
|
|
2662
|
+
while (start2 > 0 && totalLines2 + lineHeight(messages, start2 - 1, maxBubbleW, isGroup) <= msgAreaHeight) {
|
|
2594
2663
|
start2--;
|
|
2595
|
-
totalLines2 += lineHeight(messages, start2, maxBubbleW);
|
|
2664
|
+
totalLines2 += lineHeight(messages, start2, maxBubbleW, isGroup);
|
|
2596
2665
|
}
|
|
2597
2666
|
return { visibleStart: start2, visibleEnd: end2 + 1 };
|
|
2598
2667
|
}
|
|
@@ -2601,24 +2670,24 @@ function ThreadPane({
|
|
|
2601
2670
|
const targetAbove = Math.floor(msgAreaHeight * 0.4);
|
|
2602
2671
|
while (start > 0 && linesAbove < targetAbove) {
|
|
2603
2672
|
start--;
|
|
2604
|
-
linesAbove += lineHeight(messages, start, maxBubbleW);
|
|
2673
|
+
linesAbove += lineHeight(messages, start, maxBubbleW, isGroup);
|
|
2605
2674
|
}
|
|
2606
2675
|
let end = cursorIdx;
|
|
2607
|
-
let totalLines = linesAbove + lineHeight(messages, cursorIdx, maxBubbleW);
|
|
2676
|
+
let totalLines = linesAbove + lineHeight(messages, cursorIdx, maxBubbleW, isGroup);
|
|
2608
2677
|
while (end < total - 1 && totalLines < msgAreaHeight) {
|
|
2609
2678
|
end++;
|
|
2610
2679
|
if (end < messages.length) {
|
|
2611
|
-
totalLines += lineHeight(messages, end, maxBubbleW);
|
|
2680
|
+
totalLines += lineHeight(messages, end, maxBubbleW, isGroup);
|
|
2612
2681
|
} else {
|
|
2613
2682
|
totalLines += 1;
|
|
2614
2683
|
}
|
|
2615
2684
|
}
|
|
2616
2685
|
while (start > 0 && totalLines < msgAreaHeight) {
|
|
2617
2686
|
start--;
|
|
2618
|
-
totalLines += lineHeight(messages, start, maxBubbleW);
|
|
2687
|
+
totalLines += lineHeight(messages, start, maxBubbleW, isGroup);
|
|
2619
2688
|
}
|
|
2620
2689
|
return { visibleStart: start, visibleEnd: end + 1 };
|
|
2621
|
-
}, [messages, pending.length, selectedMsgIdx, msgAreaHeight, maxBubbleW]);
|
|
2690
|
+
}, [messages, pending.length, selectedMsgIdx, msgAreaHeight, maxBubbleW, isGroup]);
|
|
2622
2691
|
const visibleMessages = messages.slice(visibleStart, Math.min(visibleEnd, messages.length));
|
|
2623
2692
|
const messagesByGuid = useMemo(() => {
|
|
2624
2693
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -2748,22 +2817,27 @@ function ThreadPane({
|
|
|
2748
2817
|
}
|
|
2749
2818
|
);
|
|
2750
2819
|
}
|
|
2751
|
-
function
|
|
2820
|
+
function wrapRows(text, innerW) {
|
|
2821
|
+
let rows = 0;
|
|
2822
|
+
for (const line of text.split("\n")) {
|
|
2823
|
+
rows += Math.max(1, Math.ceil(visualWidth(line) / innerW));
|
|
2824
|
+
}
|
|
2825
|
+
return rows;
|
|
2826
|
+
}
|
|
2827
|
+
function lineHeight(messages, i, bubbleWidth, isGroup) {
|
|
2752
2828
|
if (i < 0 || i >= messages.length) return 1;
|
|
2753
2829
|
const msg = messages[i];
|
|
2754
|
-
const
|
|
2830
|
+
const firstInGroup = isGroupStart(msg, i > 0 ? messages[i - 1] : void 0);
|
|
2831
|
+
const senderPad = isGroup && firstInGroup && !msg.isFromMe ? 14 : 0;
|
|
2832
|
+
const innerW = Math.max(16, bubbleWidth - 25 - senderPad);
|
|
2755
2833
|
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
|
-
}
|
|
2834
|
+
let h = wrapRows(text, innerW);
|
|
2835
|
+
if (msg.isReply) h += 1;
|
|
2761
2836
|
if (i === 0) {
|
|
2762
2837
|
h += 1;
|
|
2763
2838
|
} else if (isDifferentDay(messages[i - 1].date, msg.date)) {
|
|
2764
2839
|
h += 2;
|
|
2765
2840
|
}
|
|
2766
|
-
if (msg.attachments && msg.attachments.length > 0) h += 1;
|
|
2767
2841
|
return h;
|
|
2768
2842
|
}
|
|
2769
2843
|
function nextGroupBoundary(messages, fromIdx) {
|
|
@@ -2836,7 +2910,6 @@ function useDevStats(visible) {
|
|
|
2836
2910
|
const lastTimeRef = useRef(Date.now());
|
|
2837
2911
|
const lastQueryMsRef = useRef(null);
|
|
2838
2912
|
useEffect(() => {
|
|
2839
|
-
if (!visible) return;
|
|
2840
2913
|
const sample = () => {
|
|
2841
2914
|
const now = Date.now();
|
|
2842
2915
|
const elapsed = now - lastTimeRef.current;
|
|
@@ -2863,9 +2936,13 @@ function useDevStats(visible) {
|
|
|
2863
2936
|
});
|
|
2864
2937
|
};
|
|
2865
2938
|
sample();
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2939
|
+
if (visible) {
|
|
2940
|
+
const timer = setInterval(sample, 2e3);
|
|
2941
|
+
timer.unref();
|
|
2942
|
+
return () => clearInterval(timer);
|
|
2943
|
+
}
|
|
2944
|
+
const unsubscribe = onMemorySample(() => sample());
|
|
2945
|
+
return unsubscribe;
|
|
2869
2946
|
}, [visible]);
|
|
2870
2947
|
const recordQueryTime = useCallback((ms) => {
|
|
2871
2948
|
lastQueryMsRef.current = Math.round(ms * 10) / 10;
|
|
@@ -3027,7 +3104,8 @@ function useImsg() {
|
|
|
3027
3104
|
if (slugRecord.isGroup) {
|
|
3028
3105
|
return slugRecord.displayName && !slugRecord.displayName.startsWith("chat") ? sendToChat(slugRecord.displayName, text) : sendToChatId(slugRecord.chatGuid, text);
|
|
3029
3106
|
}
|
|
3030
|
-
|
|
3107
|
+
const preferred = db.getPreferredSendService(slugRecord.chatIdentifier) ?? (slugRecord.service === "SMS" ? "SMS" : "iMessage");
|
|
3108
|
+
return sendMessageReliable(slugRecord.chatIdentifier, text, preferred);
|
|
3031
3109
|
},
|
|
3032
3110
|
[getDb]
|
|
3033
3111
|
);
|
|
@@ -3051,9 +3129,12 @@ function useImsg() {
|
|
|
3051
3129
|
error: `Ambiguous: ${resolution.candidates.length} matches for "${input}". Pick a specific handle.`
|
|
3052
3130
|
};
|
|
3053
3131
|
}
|
|
3054
|
-
|
|
3132
|
+
const db = getDb();
|
|
3133
|
+
const conv = await db.findChatByHandle(resolution.handle);
|
|
3134
|
+
const preferred = conv && !conv.isGroupChat ? db.getPreferredSendService(conv.chatIdentifier) ?? (conv.serviceType === "SMS" ? "SMS" : "iMessage") : void 0;
|
|
3135
|
+
return sendMessageReliable(resolution.handle, text, preferred);
|
|
3055
3136
|
},
|
|
3056
|
-
[resolveRecipientInput]
|
|
3137
|
+
[resolveRecipientInput, getDb]
|
|
3057
3138
|
);
|
|
3058
3139
|
const refresh = useCallback(() => {
|
|
3059
3140
|
getDb().scheduleBackgroundRefresh();
|
|
@@ -3124,6 +3205,7 @@ const initialState = {
|
|
|
3124
3205
|
status: "Loading...",
|
|
3125
3206
|
numBuffer: "",
|
|
3126
3207
|
showDevStats: false,
|
|
3208
|
+
drawerAttachmentIdx: 0,
|
|
3127
3209
|
conversationLoadedCount: 0,
|
|
3128
3210
|
conversationLoadingMore: false,
|
|
3129
3211
|
messageOldestLoadedId: null,
|
|
@@ -3382,9 +3464,11 @@ function reducer(state, action) {
|
|
|
3382
3464
|
case "EXIT_FILTER":
|
|
3383
3465
|
return { ...state, mode: "browse", filterQuery: "" };
|
|
3384
3466
|
case "OPEN_DRAWER":
|
|
3385
|
-
return { ...state, mode: "drawer" };
|
|
3467
|
+
return { ...state, mode: "drawer", drawerAttachmentIdx: 0 };
|
|
3386
3468
|
case "CLOSE_DRAWER":
|
|
3387
3469
|
return { ...state, mode: "browse" };
|
|
3470
|
+
case "SET_DRAWER_ATTACHMENT":
|
|
3471
|
+
return { ...state, drawerAttachmentIdx: Math.max(0, action.index) };
|
|
3388
3472
|
case "SET_NUM_BUFFER":
|
|
3389
3473
|
return { ...state, numBuffer: action.value };
|
|
3390
3474
|
case "TOGGLE_DEV_STATS":
|
|
@@ -3476,8 +3560,8 @@ function App() {
|
|
|
3476
3560
|
const commands2 = useMemo(() => allCommands(state), [state]);
|
|
3477
3561
|
const commandCtx = useMemo(() => ({ state, dispatch, imsg }), [state, imsg]);
|
|
3478
3562
|
const loadMessages = useCallback(
|
|
3479
|
-
async (idx) => {
|
|
3480
|
-
const conv = state.conversations[idx];
|
|
3563
|
+
async (idx, convOverride) => {
|
|
3564
|
+
const conv = convOverride ?? state.conversations[idx];
|
|
3481
3565
|
if (!conv) return;
|
|
3482
3566
|
dispatch({
|
|
3483
3567
|
type: "SET_LOADING",
|
|
@@ -3498,11 +3582,15 @@ function App() {
|
|
|
3498
3582
|
dispatch({ type: "SET_CONVERSATIONS", data: convs });
|
|
3499
3583
|
if (convs.length > 0) {
|
|
3500
3584
|
const prevSlug = selected?.threadSlug;
|
|
3585
|
+
let targetIdx = Math.min(Math.max(state.selectedIdx, 0), convs.length - 1);
|
|
3501
3586
|
if (prevSlug) {
|
|
3502
3587
|
const idx = convs.findIndex((c) => c.threadSlug === prevSlug);
|
|
3503
|
-
if (idx >= 0)
|
|
3588
|
+
if (idx >= 0) {
|
|
3589
|
+
dispatch({ type: "SELECT", index: idx, visibleCount: sidebarVisibleCount });
|
|
3590
|
+
targetIdx = idx;
|
|
3591
|
+
}
|
|
3504
3592
|
}
|
|
3505
|
-
await loadMessages(
|
|
3593
|
+
await loadMessages(targetIdx, convs[targetIdx]);
|
|
3506
3594
|
}
|
|
3507
3595
|
imsg.refresh();
|
|
3508
3596
|
dispatch({ type: "SET_LOADING", loading: false, status: "" });
|
|
@@ -3527,11 +3615,11 @@ function App() {
|
|
|
3527
3615
|
selected.chatIdentifier,
|
|
3528
3616
|
state.messageOldestLoadedId
|
|
3529
3617
|
);
|
|
3530
|
-
|
|
3618
|
+
const newOldestId = minMessageId(olderMsgs) ?? -1;
|
|
3619
|
+
if (olderMsgs.length === 0 || newOldestId < 0 || newOldestId >= state.messageOldestLoadedId) {
|
|
3531
3620
|
dispatch({ type: "PREPEND_MESSAGES", data: [], oldestId: -1 });
|
|
3532
3621
|
return;
|
|
3533
3622
|
}
|
|
3534
|
-
const newOldestId = minMessageId(olderMsgs) ?? -1;
|
|
3535
3623
|
dispatch({ type: "PREPEND_MESSAGES", data: olderMsgs, oldestId: newOldestId });
|
|
3536
3624
|
}, [imsg, selected, state.messageLoadingOlder, state.messageOldestLoadedId]);
|
|
3537
3625
|
useEffect(() => {
|
|
@@ -3590,11 +3678,19 @@ function App() {
|
|
|
3590
3678
|
try {
|
|
3591
3679
|
const msgs = await imsg.loadMessages(selected.chatIdentifier);
|
|
3592
3680
|
const found = msgs.some((m) => m.isFromMe && m.text?.includes(text));
|
|
3593
|
-
|
|
3681
|
+
const failed = msgs.some(
|
|
3682
|
+
(m) => m.isFromMe && m.sendError !== void 0 && m.text?.includes(text)
|
|
3683
|
+
);
|
|
3684
|
+
if (failed) {
|
|
3685
|
+
dispatch({ type: "SET_MESSAGES", data: msgs });
|
|
3686
|
+
dispatch({ type: "FAIL_PENDING", text });
|
|
3687
|
+
} else if (found) {
|
|
3594
3688
|
dispatch({ type: "SET_MESSAGES", data: msgs });
|
|
3595
3689
|
dispatch({ type: "RESOLVE_PENDING", text });
|
|
3596
3690
|
} else if (attempt < 7) {
|
|
3597
3691
|
pollTimerRef.current = setTimeout(poll, 1500);
|
|
3692
|
+
} else {
|
|
3693
|
+
dispatch({ type: "FAIL_PENDING", text });
|
|
3598
3694
|
}
|
|
3599
3695
|
} catch {
|
|
3600
3696
|
dispatch({ type: "FAIL_PENDING", text });
|
|
@@ -3681,6 +3777,9 @@ function App() {
|
|
|
3681
3777
|
}
|
|
3682
3778
|
return;
|
|
3683
3779
|
}
|
|
3780
|
+
if (state.mode === "compose-new") {
|
|
3781
|
+
return;
|
|
3782
|
+
}
|
|
3684
3783
|
if (state.mode === "confirm") {
|
|
3685
3784
|
if (key.return) {
|
|
3686
3785
|
await sendMessage();
|
|
@@ -3690,10 +3789,29 @@ function App() {
|
|
|
3690
3789
|
return;
|
|
3691
3790
|
}
|
|
3692
3791
|
if (state.mode === "drawer") {
|
|
3792
|
+
const attCount = selectedMsg?.attachments?.length ?? 0;
|
|
3693
3793
|
if (key.escape || input === "q") {
|
|
3694
3794
|
dispatch({ type: "CLOSE_DRAWER" });
|
|
3795
|
+
} else if ((input === "j" || key.downArrow) && attCount > 1) {
|
|
3796
|
+
dispatch({
|
|
3797
|
+
type: "SET_DRAWER_ATTACHMENT",
|
|
3798
|
+
index: Math.min(state.drawerAttachmentIdx + 1, attCount - 1)
|
|
3799
|
+
});
|
|
3800
|
+
} else if ((input === "k" || key.upArrow) && attCount > 1) {
|
|
3801
|
+
dispatch({ type: "SET_DRAWER_ATTACHMENT", index: state.drawerAttachmentIdx - 1 });
|
|
3695
3802
|
} else if (input === "o" && selectedMsg) {
|
|
3696
|
-
openAttachment(selectedMsg);
|
|
3803
|
+
openAttachment(selectedMsg, state.drawerAttachmentIdx);
|
|
3804
|
+
} else if (input === "s" && selectedMsg) {
|
|
3805
|
+
saveAttachment(selectedMsg, state.drawerAttachmentIdx);
|
|
3806
|
+
} else if (input === "y" && selectedMsg) {
|
|
3807
|
+
const att = selectedMsg.attachments?.[state.drawerAttachmentIdx];
|
|
3808
|
+
if (att?.filename) {
|
|
3809
|
+
const filepath = att.filename.replace(/^~/, process.env.HOME ?? "~");
|
|
3810
|
+
execSync("pbcopy", { input: filepath });
|
|
3811
|
+
dispatch({ type: "SET_STATUS", status: "Attachment path copied." });
|
|
3812
|
+
} else {
|
|
3813
|
+
dispatch({ type: "SET_STATUS", status: "No attachment to copy." });
|
|
3814
|
+
}
|
|
3697
3815
|
}
|
|
3698
3816
|
return;
|
|
3699
3817
|
}
|
|
@@ -3938,9 +4056,16 @@ function App() {
|
|
|
3938
4056
|
const prev = prevGroupBoundary(state.messages, state.selectedMsgIdx);
|
|
3939
4057
|
dispatch({ type: "SELECT_MSG", index: prev });
|
|
3940
4058
|
} else if (input === "o") {
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
4059
|
+
const msg = state.selectedMsgIdx >= 0 ? state.messages[state.selectedMsgIdx] : void 0;
|
|
4060
|
+
if (msg && (msg.attachments?.length ?? 0) > 1) {
|
|
4061
|
+
dispatch({ type: "OPEN_DRAWER" });
|
|
4062
|
+
dispatch({
|
|
4063
|
+
type: "SET_STATUS",
|
|
4064
|
+
status: `${msg.attachments?.length} attachments — j/k select, o open, s save`
|
|
4065
|
+
});
|
|
4066
|
+
} else {
|
|
4067
|
+
openAttachment(msg);
|
|
4068
|
+
}
|
|
3944
4069
|
}
|
|
3945
4070
|
}
|
|
3946
4071
|
});
|
|
@@ -4037,7 +4162,7 @@ function App() {
|
|
|
4037
4162
|
state.selectionAnchor,
|
|
4038
4163
|
selected
|
|
4039
4164
|
]);
|
|
4040
|
-
function openAttachment(msg) {
|
|
4165
|
+
function openAttachment(msg, attIdx = 0) {
|
|
4041
4166
|
if (!msg) {
|
|
4042
4167
|
dispatch({ type: "SET_STATUS", status: "No message selected." });
|
|
4043
4168
|
return;
|
|
@@ -4046,7 +4171,7 @@ function App() {
|
|
|
4046
4171
|
dispatch({ type: "SET_STATUS", status: "No attachment on this message." });
|
|
4047
4172
|
return;
|
|
4048
4173
|
}
|
|
4049
|
-
const att = msg.attachments[0];
|
|
4174
|
+
const att = msg.attachments[attIdx] ?? msg.attachments[0];
|
|
4050
4175
|
if (!att.filename) {
|
|
4051
4176
|
dispatch({ type: "SET_STATUS", status: "Attachment has no file path." });
|
|
4052
4177
|
return;
|
|
@@ -4064,6 +4189,35 @@ function App() {
|
|
|
4064
4189
|
}
|
|
4065
4190
|
});
|
|
4066
4191
|
}
|
|
4192
|
+
function saveAttachment(msg, attIdx) {
|
|
4193
|
+
const att = msg.attachments?.[attIdx];
|
|
4194
|
+
if (!att?.filename) {
|
|
4195
|
+
dispatch({ type: "SET_STATUS", status: "No attachment to save." });
|
|
4196
|
+
return;
|
|
4197
|
+
}
|
|
4198
|
+
const src = att.filename.replace(/^~/, process.env.HOME ?? "~");
|
|
4199
|
+
import("node:fs").then((fs) => {
|
|
4200
|
+
import("node:path").then((path) => {
|
|
4201
|
+
try {
|
|
4202
|
+
const base = att.transferName ?? path.basename(src);
|
|
4203
|
+
const dir = join(homedir(), "Downloads");
|
|
4204
|
+
const ext = path.extname(base);
|
|
4205
|
+
const stem = base.slice(0, base.length - ext.length);
|
|
4206
|
+
let dest = path.join(dir, base);
|
|
4207
|
+
for (let n = 1; fs.existsSync(dest); n++) {
|
|
4208
|
+
dest = path.join(dir, `${stem}-${n}${ext}`);
|
|
4209
|
+
}
|
|
4210
|
+
fs.copyFileSync(src, dest);
|
|
4211
|
+
dispatch({ type: "SET_STATUS", status: `Saved to ${dest.replace(homedir(), "~")}` });
|
|
4212
|
+
} catch (e) {
|
|
4213
|
+
dispatch({
|
|
4214
|
+
type: "SET_STATUS",
|
|
4215
|
+
status: `Save failed: ${e instanceof Error ? e.message : String(e)}`
|
|
4216
|
+
});
|
|
4217
|
+
}
|
|
4218
|
+
});
|
|
4219
|
+
});
|
|
4220
|
+
}
|
|
4067
4221
|
const handleMouse = useCallback(
|
|
4068
4222
|
(event) => {
|
|
4069
4223
|
if (event.type === "click") {
|
|
@@ -4163,7 +4317,15 @@ function App() {
|
|
|
4163
4317
|
onSubmitCompose: (text) => text.trim() && dispatch({ type: "CONFIRM_SEND" })
|
|
4164
4318
|
}
|
|
4165
4319
|
),
|
|
4166
|
-
state.mode === "drawer" && selectedMsg && /* @__PURE__ */ jsx(
|
|
4320
|
+
state.mode === "drawer" && selectedMsg && /* @__PURE__ */ jsx(
|
|
4321
|
+
MessageDrawer,
|
|
4322
|
+
{
|
|
4323
|
+
message: selectedMsg,
|
|
4324
|
+
width: drawerWidth,
|
|
4325
|
+
height: bodyHeight,
|
|
4326
|
+
selectedAttachmentIdx: state.drawerAttachmentIdx
|
|
4327
|
+
}
|
|
4328
|
+
),
|
|
4167
4329
|
state.showDevStats && /* @__PURE__ */ jsx(DevStats, { stats: devStats, width: devStatsWidth })
|
|
4168
4330
|
] }),
|
|
4169
4331
|
state.mode === "date-jump" && /* @__PURE__ */ jsx(
|