Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions src/commands/messages/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,20 @@ export default class MessagesList extends BaseCommand {
const client = createApiClient(token);

try {
const data = await client.chats.messages.list(args.chatId, {
limit: flags.limit,
cursor: flags.cursor,
});
// Fetch the chat alongside the messages so we can render
// "you → counterparty" / "counterparty → you" in the list.
const [data, chat] = await Promise.all([
client.chats.messages.list(args.chatId, {
limit: flags.limit,
cursor: flags.cursor,
}),
client.chats.retrieve(args.chatId).catch(() => undefined),
]);

if (flags.json) {
this.log(JSON.stringify(data, null, 2));
this.log(JSON.stringify({ messages: data.messages, next_cursor: data.next_cursor }, null, 2));
} else {
this.log(formatMessagesList(data));
this.log(formatMessagesList(data, chat));
}
} catch (e) {
this.error(`Failed to list messages: ${e instanceof Error ? e.message : String(e)}`);
Expand Down
4 changes: 2 additions & 2 deletions src/commands/phonenumbers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@
const token = requireToken(flags.token, config);
const client = createApiClient(token);

let phones: { phone_number: string }[];
let phones: { id: string; phone_number: string }[];
try {
const data = await client.phoneNumbers.list();
phones = (data as any).phone_numbers || [];

Check warning on line 50 in src/commands/phonenumbers.ts

View workflow job for this annotation

GitHub Actions / test

Unexpected any. Specify a different type
} catch (e) {
this.error(`Failed to list phone numbers: ${e instanceof Error ? e.message : String(e)}`);
}
Expand All @@ -70,7 +70,7 @@
this.log(`\n ${chalk.bold('Your phone numbers')}\n`);
for (const p of phones) {
const isDefault = p.phone_number === config.fromPhone;
this.log(` ${this.formatPhone(p.phone_number)}${isDefault ? chalk.green(' ← default') : ''}`);
this.log(` ${chalk.cyan(p.id)} ${this.formatPhone(p.phone_number)}${isDefault ? chalk.green(' ← default') : ''}`);
}
if (!config.fromPhone && phones.length > 1) {
this.log(`\n ${chalk.dim('Tip: run')} ${chalk.cyan('linq phonenumbers set')} ${chalk.dim('to pick a default.')}`);
Expand Down
47 changes: 40 additions & 7 deletions src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ function fmtDateAbsolute(iso: string | null | undefined): string {
return new Date(iso).toLocaleString();
}

function fmtDateShort(iso: string | null | undefined): string {
if (!iso) return '–';
const d = new Date(iso);
const now = new Date();
const sameYear = d.getFullYear() === now.getFullYear();
return d.toLocaleString(undefined, {
month: 'short',
day: 'numeric',
...(sameYear ? {} : { year: 'numeric' }),
hour: 'numeric',
minute: '2-digit',
});
}

function fmtPhone(phone: string): string {
// Format US numbers as +1 (XXX) XXX-XXXX
const match = phone.match(/^\+1(\d{3})(\d{3})(\d{4})$/);
Expand Down Expand Up @@ -77,7 +91,7 @@ export function formatPhoneNumbers(data: { phone_numbers: PhoneNumberInfo[] | nu

const lines = ['\n ' + chalk.bold('Your phone numbers') + '\n'];
for (const p of phones) {
lines.push(` ${fmtPhone(p.phone_number)}`);
lines.push(` ${chalk.cyan(p.id)} ${fmtPhone(p.phone_number)}`);
}
lines.push('');
return lines.join('\n');
Expand Down Expand Up @@ -190,22 +204,41 @@ interface Message {
delivery_status?: string | null;
}

export function formatMessagesList(data: { messages: Message[]; next_cursor?: string | null }): string {
export function formatMessagesList(
data: { messages: Message[]; next_cursor?: string | null },
chat?: Chat,
): string {
const msgs = data.messages;
if (msgs.length === 0) return 'No messages found.';

const rows = msgs.map((m) => {
// Counterparty = everyone in the chat who isn't me. Used when I sent the
// message ("you → them"). If we don't have the chat, fall back to "you →".
const counterparty = (chat?.handles || [])
.filter((h) => !h.is_me)
.map((h) => fmtPhone(h.handle))
.join(', ');

const direction = (m: Message): string => {
if (m.is_from_me) {
const right = counterparty ? ` ${chalk.bold(counterparty)}` : '';
return `${chalk.dim('you')} ${chalk.dim('→')}${right}`;
}
const sender = m.from_handle?.handle
? fmtPhone(m.from_handle.handle)
: m.from ? fmtPhone(m.from) : (m.is_from_me ? chalk.dim('you') : '?');
: m.from ? fmtPhone(m.from) : '?';
return `${chalk.bold(sender)} ${chalk.dim('→')} ${chalk.dim('you')}`;
};

const rows = msgs.map((m) => {
const id = chalk.cyan(m.id);
const when = chalk.dim(fmtDateShort(m.created_at));
const body = (m.parts || [])
.map((p) => (p?.type === 'text' ? p.value : `[${p?.type || 'media'}]`))
.join(' ')
|| '';
const status = m.delivery_status || (m.is_read ? 'read' : m.is_delivered ? 'delivered' : 'sent');
const direction = m.is_from_me ? chalk.dim('→') : chalk.green('←');
return `${chalk.dim(fmtDate(m.created_at))} ${direction} ${chalk.bold(sender)} ${truncate(body, 50)} ${statusColor(status)}`;
return `${id} ${when} ${direction(m)} ${truncate(body, 60)}`;
});

const lines = [...rows];
if (data.next_cursor) {
lines.push(chalk.dim(`\nMore results available. Use --cursor ${data.next_cursor}`));
Expand Down
8 changes: 5 additions & 3 deletions test/commands/messages/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ describe('messages list', () => {
const cmd = new MessagesList(['chat-123'], config);
await cmd.run();

expect(mockFetch).toHaveBeenCalledOnce();
const [url] = mockFetch.mock.calls[0];
expect(url).toContain('/v3/chats/chat-123/messages');
// messages list now also fetches the chat (to render "you → counterparty")
expect(mockFetch).toHaveBeenCalledTimes(2);
const urls = mockFetch.mock.calls.map(([u]) => u.toString());
expect(urls.some((u) => u.includes('/v3/chats/chat-123/messages'))).toBe(true);
expect(urls.some((u) => u.includes('/v3/chats/chat-123') && !u.includes('/messages'))).toBe(true);
});

it('handles pagination parameters', async () => {
Expand Down
Loading