cli-whatsapp 0.1.8 → 0.1.12

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.
@@ -210,7 +210,7 @@ export function pickFromList(title, allItems, initialFilter = '') {
210
210
  return void ((index = Math.min(items.length - 1, index + 1)), render());
211
211
  if (key.name === 'return') {
212
212
  const it = items[index];
213
- return done(it ? it.value : null);
213
+ return done(it ? (it.id ?? it.value) : null);
214
214
  }
215
215
  if (key.name === 'backspace') {
216
216
  if (!filter)
@@ -1,7 +1,7 @@
1
1
  import readline, { createInterface } from 'node:readline';
2
2
  import { readdirSync, statSync, existsSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { join, basename } from 'node:path';
5
5
  import { chats, contacts, search, history } from "./read.js";
6
6
  import { reply, buildMediaContent, sendContent } from "./send.js";
7
7
  import { media, downloadWithSocket } from "./media.js";
@@ -11,10 +11,10 @@ import { recent, pin, unpin, archive, unarchive } from "./manage.js";
11
11
  import { status } from "./auth.js";
12
12
  import { connect, hasSession } from "../../whatsapp/client.js";
13
13
  import { registerPersistence, extractText, isGroupJid, messageType } from "../../whatsapp/events.js";
14
- import { listChats, listContacts, findChat, upsertChat, getMessages, getMessageById, localIdForWaMessage, countMessages, } from "../../db/sqlite.js";
14
+ import { listChats, listContacts, listMedia, findChat, upsertChat, getMessages, getMessageById, localIdForWaMessage, countMessages, } from "../../db/sqlite.js";
15
15
  import { resolveTarget } from "../../utils/jid.js";
16
16
  import { loadConfig, saveConfig } from "../../config/index.js";
17
- import { c, displayName, chatNumber, formatTime, truncate, renderMessages } from "../../utils/format.js";
17
+ import { c, displayName, chatNumber, cleanSender, formatTime, truncate, renderMessages } from "../../utils/format.js";
18
18
  import { print, warn, printError } from "../../utils/ui.js";
19
19
  import { notify } from "../../utils/notify.js";
20
20
  // ---- tab completion --------------------------------------------------------
@@ -29,7 +29,8 @@ const SLASH_ITEMS = [
29
29
  { value: '/send', desc: 'send a message to a chat', args: true },
30
30
  { value: '/sendfile', desc: 'send a file (media)', args: true },
31
31
  { value: '/reply', desc: 'reply to a message by #id', args: true },
32
- { value: '/media', desc: 'list cached media' },
32
+ { value: '/media', desc: 'list cached media (this chat, or all)' },
33
+ { value: '/dl', desc: 'pick recent media to download & open' },
33
34
  { value: '/pin', desc: 'pin a chat to the top', args: true },
34
35
  { value: '/unpin', desc: 'unpin a chat', args: true },
35
36
  { value: '/archive', desc: 'archive (hide) a chat', args: true },
@@ -263,7 +264,20 @@ async function sendTo(state, jid, name, content, meta, replaceInput = false) {
263
264
  return;
264
265
  try {
265
266
  await sendContent(conn.sock, jid, content, meta);
266
- const echo = meta.type === 'text' ? meta.previewText : `[${meta.type}] ${meta.previewText}`;
267
+ // Build the echo: text the message; media "[type] filename" plus any
268
+ // caption (never "[image] [image]"). The filename tells you which file.
269
+ let echo;
270
+ if (meta.type === 'text') {
271
+ echo = meta.previewText;
272
+ }
273
+ else {
274
+ const fname = meta.mediaPath ? basename(meta.mediaPath) : '';
275
+ const caption = meta.previewText.startsWith('[') ? '' : meta.previewText;
276
+ echo =
277
+ `${c.dim(`[${meta.type}]`)}` +
278
+ (fname ? ` ${fname}` : '') +
279
+ (caption ? ` ${c.dim('—')} ${caption}` : '');
280
+ }
267
281
  // Replace the just-typed input with the bubble so it shows once — but only
268
282
  // when we were already live (no "connecting…" lines were printed in between).
269
283
  if (replaceInput && wasLive)
@@ -301,6 +315,34 @@ async function downloadAndOpen(state, idRaw) {
301
315
  warn(`Download failed: ${err.message}`);
302
316
  }
303
317
  }
318
+ /** `/dl` with no id: pick from recent, not-yet-downloaded media (this chat if
319
+ * open, else all), newest first — arrow-select, no id to type. */
320
+ async function pickMediaToDownload(state) {
321
+ const rows = listMedia(null, 100, state.active?.jid, true); // undownloaded only
322
+ if (!rows.length) {
323
+ warn(state.active ? 'No new media in this chat to download.' : 'No new media to download.');
324
+ return;
325
+ }
326
+ const time24h = loadConfig().preferences.time24h;
327
+ const items = rows.map((m) => {
328
+ const label = m.text
329
+ ? truncate(m.text.replace(/\s+/g, ' '), 32)
330
+ : m.mediaPath
331
+ ? basename(m.mediaPath)
332
+ : '';
333
+ return {
334
+ value: `[${m.type}]${label ? ' ' + label : ''}`,
335
+ desc: `${m.fromMe ? 'you' : cleanSender(m.sender)} · ${formatTime(m.timestamp, time24h)}`,
336
+ id: String(m.id),
337
+ };
338
+ });
339
+ const title = state.active
340
+ ? `Media with ${displayName(state.active)} — ⏎ download & open`
341
+ : 'All media — ⏎ download & open';
342
+ const picked = await pickFromList(title, items, '');
343
+ if (picked)
344
+ await downloadAndOpen(state, picked);
345
+ }
304
346
  // ---- chat context ----------------------------------------------------------
305
347
  function openChat(state, target) {
306
348
  // Resolve against chats, contacts, or a raw number/jid.
@@ -477,8 +519,10 @@ async function handle(state, raw) {
477
519
  case 'download':
478
520
  case 'dl':
479
521
  case 'open-media':
480
- if (need(args[0], 'id'))
522
+ if (args[0])
481
523
  await downloadAndOpen(state, args[0]);
524
+ else
525
+ await pickMediaToDownload(state); // no id → pick from recent media
482
526
  return;
483
527
  case 'status':
484
528
  status();
package/dist/db/sqlite.js CHANGED
@@ -249,7 +249,7 @@ export function importLidMappings() {
249
249
  return n;
250
250
  }
251
251
  /** Media messages, optionally filtered by type (image/video/audio/document/sticker). */
252
- export function listMedia(types, limit = 50, chatJid) {
252
+ export function listMedia(types, limit = 50, chatJid, undownloadedOnly = false) {
253
253
  const kinds = types && types.length ? types : ['image', 'video', 'audio', 'document', 'sticker'];
254
254
  const placeholders = kinds.map(() => '?').join(',');
255
255
  const params = [...kinds];
@@ -258,6 +258,8 @@ export function listMedia(types, limit = 50, chatJid) {
258
258
  where += ' AND chatJid = ?';
259
259
  params.push(chatJid);
260
260
  }
261
+ if (undownloadedOnly)
262
+ where += " AND (mediaPath IS NULL OR mediaPath = '')";
261
263
  params.push(limit);
262
264
  return getDb()
263
265
  .prepare(`SELECT * FROM messages WHERE ${where} ORDER BY timestamp DESC LIMIT ?`)
@@ -79,13 +79,16 @@ export function renderChatList(chats, time24h = true) {
79
79
  export function renderMessages(messages, time24h = true) {
80
80
  if (messages.length === 0)
81
81
  return c.dim('No messages.');
82
+ const MEDIA = ['image', 'video', 'audio', 'document', 'sticker'];
82
83
  return messages
83
84
  .map((m) => {
84
85
  const time = c.gray(formatTime(m.timestamp, time24h));
85
86
  const who = m.fromMe ? c.green('me') : c.cyan(cleanSender(m.sender));
87
+ const isMedia = m.type ? MEDIA.includes(m.type) : false;
86
88
  const body = m.text ?? (m.type ? c.dim(`[${m.type}]`) : c.dim('[no text]'));
87
- const id = c.dim(`#${m.id}`);
88
- return `${time} ${who}: ${body} ${id}`;
89
+ // Media → an actionable "/dl <id>"; text → a dim #id (for /reply).
90
+ const tail = isMedia && !m.fromMe ? c.green(`/dl ${m.id}`) : c.dim(`#${m.id}`);
91
+ return `${time} ${who}: ${body} ${tail}`;
89
92
  })
90
93
  .join('\n');
91
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-whatsapp",
3
- "version": "0.1.8",
3
+ "version": "0.1.12",
4
4
  "description": "A lightweight, fast, terminal-first WhatsApp client. Read, search, send, media, groups, and live notifications — from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {