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
159 changes: 76 additions & 83 deletions src/lib/refs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,34 +124,49 @@ describe('parseCommsUrl', () => {
expect(result).toEqual({ workspaceId: 12345, channelId: '7YpL3oZ4kZ9vP7Q1tR2sX3z' })
})

it('parses short thread URL', () => {
const result = parseCommsUrl('https://comms.todoist.com/12345/ch/CH1/t/TH1')
expect(result).toEqual({ workspaceId: 12345, channelId: 'CH1', threadId: 'TH1' })
})

it('parses thread URL', () => {
const result = parseCommsUrl('https://comms.todoist.com/a/12345/ch/CH1/t/TH1')
expect(result).toEqual({ workspaceId: 12345, channelId: 'CH1', threadId: 'TH1' })
})

it('parses short thread with comment URL', () => {
const result = parseCommsUrl('https://comms.todoist.com/12345/ch/CH1/t/TH1/c/CM1')
expect(result).toEqual({
workspaceId: 12345,
channelId: 'CH1',
threadId: 'TH1',
commentId: 'CM1',
})
})

it('parses thread with comment URL', () => {
const result = parseCommsUrl('https://comms.todoist.com/a/12345/ch/CH1/t/TH1/c/CM1')
expect(result).toEqual({
workspaceId: 12345,
channelId: 'CH1',
threadId: 'TH1',
commentId: 'CM1',
})
it.each([
[
'short thread URL',
'https://comms.todoist.com/12345/ch/CH1/t/TH1',
{ workspaceId: 12345, channelId: 'CH1', threadId: 'TH1' },
],
[
'thread URL',
'https://comms.todoist.com/a/12345/ch/CH1/t/TH1',
{ workspaceId: 12345, channelId: 'CH1', threadId: 'TH1' },
],
[
'short thread with comment URL',
'https://comms.todoist.com/12345/ch/CH1/t/TH1/c/CM1',
{ workspaceId: 12345, channelId: 'CH1', threadId: 'TH1', commentId: 'CM1' },
],
[
'thread with comment URL',
'https://comms.todoist.com/a/12345/ch/CH1/t/TH1/c/CM1',
{ workspaceId: 12345, channelId: 'CH1', threadId: 'TH1', commentId: 'CM1' },
],
[
'inbox thread URL',
'https://comms.todoist.com/12345/inbox/t/TH1/',
{ workspaceId: 12345, threadId: 'TH1' },
],
[
'inbox thread with comment URL',
'https://comms.todoist.com/12345/inbox/t/TH1/c/CM1',
{ workspaceId: 12345, threadId: 'TH1', commentId: 'CM1' },
],
[
'saved thread URL',
'https://comms.todoist.com/12345/saved/t/TH1',
{ workspaceId: 12345, threadId: 'TH1' },
],
[
'people URL as workspace-only',
'https://comms.todoist.com/12345/people/u/678',
{ workspaceId: 12345 },
],
])('parses %s', (_description, url, expected) => {
expect(parseCommsUrl(url)).toEqual(expected)
})

it('parses conversation URL', () => {
Expand Down Expand Up @@ -270,12 +285,16 @@ describe('resolveThreadId', () => {
expect(resolveThreadId('CbjxNkWHJBwcaVkoTCRgM')).toBe('CbjxNkWHJBwcaVkoTCRgM')
})

it('resolves thread URLs', () => {
expect(resolveThreadId('https://comms.todoist.com/a/12345/ch/CH1/t/TH1')).toBe('TH1')
})

it('resolves thread URLs with comment suffix', () => {
expect(resolveThreadId('https://comms.todoist.com/a/12345/ch/CH1/t/TH1/c/CM1')).toBe('TH1')
it.each([
['thread URL', 'https://comms.todoist.com/a/12345/ch/CH1/t/TH1'],
['thread URL with comment suffix', 'https://comms.todoist.com/a/12345/ch/CH1/t/TH1/c/CM1'],
['inbox thread URL', 'https://comms.todoist.com/12345/inbox/t/TH1/'],
[
'inbox thread URL with comment suffix',
'https://comms.todoist.com/12345/inbox/t/TH1/c/CM1',
],
])('resolves %s', (_description, url) => {
expect(resolveThreadId(url)).toBe('TH1')
})

it('throws on invalid refs', () => {
Expand Down Expand Up @@ -526,55 +545,29 @@ describe('partitionNotifyIds', () => {
})

describe('classifyCommsUrl', () => {
it('classifies thread URL', () => {
expect(classifyCommsUrl('https://comms.todoist.com/a/20/ch/CH1/t/TH1')).toEqual({
entityType: 'thread',
url: 'https://comms.todoist.com/a/20/ch/CH1/t/TH1',
})
})

it('classifies thread+comment URL as comment', () => {
expect(classifyCommsUrl('https://comms.todoist.com/a/20/ch/CH1/t/TH1/c/CM1')).toEqual({
entityType: 'comment',
url: 'https://comms.todoist.com/a/20/ch/CH1/t/TH1/c/CM1',
})
})

it('classifies conversation URL', () => {
expect(classifyCommsUrl('https://comms.todoist.com/a/20/msg/CV1')).toEqual({
entityType: 'conversation',
url: 'https://comms.todoist.com/a/20/msg/CV1',
})
})

it('classifies short conversation URL', () => {
expect(classifyCommsUrl('https://comms.todoist.com/20/msg/CV1')).toEqual({
entityType: 'conversation',
url: 'https://comms.todoist.com/20/msg/CV1',
})
})

it('classifies message URL', () => {
expect(classifyCommsUrl('https://comms.todoist.com/a/20/msg/CV1/m/MS1')).toEqual({
entityType: 'message',
url: 'https://comms.todoist.com/a/20/msg/CV1/m/MS1',
})
})

it('returns null for workspace-only URL', () => {
expect(classifyCommsUrl('https://comms.todoist.com/a/20')).toBeNull()
})

it('returns null for channel-only URL', () => {
expect(classifyCommsUrl('https://comms.todoist.com/a/20/ch/CH1')).toBeNull()
})

it('returns null for non-Comms URL', () => {
expect(classifyCommsUrl('https://google.com/a/20/t/200')).toBeNull()
})

it('returns null for invalid string', () => {
expect(classifyCommsUrl('not-a-url')).toBeNull()
it.each([
['thread URL', 'https://comms.todoist.com/a/20/ch/CH1/t/TH1', 'thread'],
['thread+comment URL', 'https://comms.todoist.com/a/20/ch/CH1/t/TH1/c/CM1', 'comment'],
['inbox thread URL', 'https://comms.todoist.com/20/inbox/t/TH1/', 'thread'],
['inbox thread+comment URL', 'https://comms.todoist.com/20/inbox/t/TH1/c/CM1', 'comment'],
['conversation URL', 'https://comms.todoist.com/a/20/msg/CV1', 'conversation'],
['short conversation URL', 'https://comms.todoist.com/20/msg/CV1', 'conversation'],
['message URL', 'https://comms.todoist.com/a/20/msg/CV1/m/MS1', 'message'],
] as const)('classifies %s', (_description, url, entityType) => {
expect(classifyCommsUrl(url)).toEqual({ entityType, url })
})

it.each([
['inbox root URL', 'https://comms.todoist.com/20/inbox'],
['inbox done URL', 'https://comms.todoist.com/20/inbox/done'],
['inbox done thread-like URL', 'https://comms.todoist.com/20/inbox/done/t/TH1'],
['workspace-only URL', 'https://comms.todoist.com/a/20'],
['channel-only URL', 'https://comms.todoist.com/a/20/ch/CH1'],
['malformed account URL', 'https://comms.todoist.com/a/ch/CH1/t/TH1'],
['non-Comms URL', 'https://google.com/a/20/t/200'],
['invalid string', 'not-a-url'],
])('returns null for %s', (_description, url) => {
expect(classifyCommsUrl(url)).toBeNull()
})
})

Expand Down
47 changes: 29 additions & 18 deletions src/lib/refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,27 +112,38 @@ export function parseCommsUrl(url: string): ParsedCommsUrl | null {
routeStart = 1
}

for (let index = routeStart; index < segments.length - 1; index += 2) {
const value = segments[index + 1]
switch (segments[index]) {
case 'ch':
result.channelId = value
break
case 't':
result.threadId = value
break
case 'c':
result.commentId = value
break
case 'msg':
result.conversationId = value
break
case 'm':
result.messageId = value
break
const parseRoutePairs = (start: number) => {
for (let index = start; index < segments.length - 1; index += 2) {
const value = segments[index + 1]
switch (segments[index]) {
case 'ch':
result.channelId = value
break
case 't':
result.threadId = value
break
case 'c':
result.commentId = value
break
case 'msg':
result.conversationId = value
break
case 'm':
result.messageId = value
break
}
}
}

if (
(segments[routeStart] === 'inbox' || segments[routeStart] === 'saved') &&
segments[routeStart + 1] === 't'
) {
parseRoutePairs(routeStart + 1)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 P2 This reuses the generic pair parser for inbox/t and saved/t, so malformed URLs with extra route pairs are now accepted. For example, classifyCommsUrl('https://comms.todoist.com/20/inbox/t/TH1/msg/CV1') will return conversation and tdc view will route it, even though this isn't a valid conversation URL. Please parse these routes explicitly as t/{thread} with only an optional c/{comment} suffix, and leave any other trailing segments unclassified.

} else if (segments[routeStart] !== 'inbox' && segments[routeStart] !== 'saved') {
parseRoutePairs(routeStart)
}

return Object.keys(result).length > 0 ? result : null
} catch {
return null
Expand Down
Loading