Skip to content

Commit f989207

Browse files
chrisbobbegnprice
authored andcommitted
internalLinks [nfc]: Dissolve getLinkType / LinkType indirection
This puts the logic to recognize a "stream link" right next to the logic for building a stream narrow, and likewise for topic narrows, etc. Now it's quicker to read what `Narrow` we construct for a given `url` param. We don't have to click through `getLinkType` and wonder whether anything surprising is done at that layer.
1 parent 082abdd commit f989207

File tree

2 files changed

+86
-91
lines changed

2 files changed

+86
-91
lines changed

src/utils/__tests__/internalLinks-test.js

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,27 @@ import invariant from 'invariant';
66
/* eslint jest/expect-expect: ["error", { "assertFunctionNames": ["expect", "check", "expectStream"] }] */
77

88
import type { Stream } from '../../types';
9-
import { streamNarrow, topicNarrow, pmNarrowFromUsersUnsafe, STARRED_NARROW } from '../narrow';
9+
import {
10+
streamNarrow,
11+
topicNarrow,
12+
pmNarrowFromUsersUnsafe,
13+
STARRED_NARROW,
14+
isPmNarrow,
15+
isStreamNarrow,
16+
isTopicNarrow,
17+
isSpecialNarrow,
18+
} from '../narrow';
1019
import {
1120
isNarrowLink,
12-
getLinkType,
1321
getNarrowFromLink,
1422
getNearOperandFromLink,
1523
decodeHashComponent,
1624
} from '../internalLinks';
1725
import * as eg from '../../__tests__/lib/exampleData';
1826
import { isUrlRelative } from '../url';
19-
import type { LinkType } from '../internalLinks';
27+
import { getStreamsById, getStreamsByName } from '../../subscriptions/subscriptionSelectors';
28+
import { getOwnUserId } from '../../users/userSelectors';
29+
import type { Narrow } from '../narrow';
2030

2131
const realm = new URL('https://example.com');
2232
const urlOnRealm = relativeUrl => {
@@ -121,22 +131,44 @@ describe('isNarrowLink', () => {
121131
}
122132
});
123133

124-
describe('getLinkType', () => {
125-
const mkCheck = (expected: LinkType) => hash => {
126-
test(`${hash} - should return ${expected}`, () => {
127-
expect(getLinkType(new URL(hash, realm), realm)).toBe(expected);
128-
});
134+
// TODO: Combine / dedupe with "getNarrowFromLink (part 2)" tests, below
135+
describe('getNarrowFromLink (part 1)', () => {
136+
const mkCheck = (narrowExpectation: (Narrow => boolean) | null) => hash => {
137+
const streams = [
138+
eg.makeStream({ name: 'jest' }),
139+
eg.makeStream({ name: 'stream' }),
140+
eg.makeStream({ name: 'topic' }),
141+
eg.makeStream({ name: 'mobile' }),
142+
];
143+
const baseState = eg.reduxStatePlus({ streams });
144+
const narrow = getNarrowFromLink(
145+
new URL(hash, realm),
146+
realm,
147+
getStreamsById(baseState),
148+
getStreamsByName(baseState),
149+
getOwnUserId(baseState),
150+
);
151+
152+
if (typeof narrowExpectation === 'function') {
153+
test(`${hash} - ${narrowExpectation.name} should be true`, () => {
154+
expect(narrow && narrowExpectation(narrow)).toBeTrue();
155+
});
156+
} else {
157+
test(`${hash} - should return null`, () => {
158+
expect(narrow).toBeNull();
159+
});
160+
}
129161
};
130162

131163
describe('link containing "stream" is a stream link', () => {
132-
const check = mkCheck('stream');
164+
const check = mkCheck(isStreamNarrow);
133165
['/#narrow/stream/jest', '/#narrow/stream/stream/', '/#narrow/stream/topic/'].forEach(hash =>
134166
check(hash),
135167
);
136168
});
137169

138170
describe('link containing "topic" is a topic link', () => {
139-
const check = mkCheck('topic');
171+
const check = mkCheck(isTopicNarrow);
140172
[
141173
'/#narrow/stream/jest/topic/test',
142174
'/#narrow/stream/mobile/subject/topic/near/378333',
@@ -148,7 +180,7 @@ describe('getLinkType', () => {
148180
});
149181

150182
describe('link containing "pm-with" is a PM link', () => {
151-
const check = mkCheck('pm');
183+
const check = mkCheck(isPmNarrow);
152184
[
153185
'/#narrow/pm-with/1,2-group',
154186
'/#narrow/pm-with/1,2-group/near/1',
@@ -157,14 +189,14 @@ describe('getLinkType', () => {
157189
});
158190

159191
describe('link containing "is" with valid operand is a special link', () => {
160-
const check = mkCheck('special');
192+
const check = mkCheck(isSpecialNarrow);
161193
['/#narrow/is/private', '/#narrow/is/starred', '/#narrow/is/mentioned'].forEach(hash =>
162194
check(hash),
163195
);
164196
});
165197

166-
describe('unexpected link shape gives "home"', () => {
167-
const check = mkCheck('home');
198+
describe('unexpected link shape gives null', () => {
199+
const check = mkCheck(null);
168200
[
169201
// `near` with no operand
170202
'/#narrow/stream/stream/topic/topic/near/',
@@ -198,7 +230,8 @@ describe('decodeHashComponent', () => {
198230
});
199231
});
200232

201-
describe('getNarrowFromLink', () => {
233+
// TODO: Combine / dedupe with "getNarrowFromLink (part 1)" tests above
234+
describe('getNarrowFromLink (part 2)', () => {
202235
const [userB, userC] = [eg.makeUser(), eg.makeUser()];
203236

204237
const streamGeneral = eg.makeStream({ name: 'general' });

src/utils/internalLinks.js

Lines changed: 38 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { makeUserId } from '../api/idTypes';
55
import type { Narrow, Stream, UserId, Message, Outbox, PmMessage, PmOutbox } from '../types';
66
import { topicNarrow, streamNarrow, specialNarrow, pmNarrowFromRecipients } from './narrow';
77
import { pmKeyRecipientsFromIds, recipientsOfPrivateMessage } from './recipient';
8-
import { ensureUnreachable } from '../generics';
98
import { isUrlOnRealm } from './url';
109

1110
/**
@@ -48,51 +47,6 @@ export const isNarrowLink = (url: URL, realm: URL): boolean =>
4847
&& url.search === ''
4948
&& /^#narrow\//i.test(url.hash);
5049

51-
/* PRIVATE: Exported only for tests. */
52-
export type LinkType = 'home' | 'pm' | 'topic' | 'stream' | 'special';
53-
54-
/**
55-
* PRIVATE -- exported only for tests.
56-
*
57-
* The passed `url` must appear to be a link to a Zulip narrow on the given
58-
* `realm`. In particular, `isNarrowLink(url, realm)` must be true.
59-
*/
60-
// TODO: Work out what this does, write a jsdoc for its interface, and
61-
// reimplement using URL object (not just for the realm)
62-
export const getLinkType = (url: URL, realm: URL): LinkType => {
63-
// isNarrowLink(…) is true, by jsdoc, so this call is OK.
64-
const hashSegments = getHashSegmentsFromNarrowLink(url, realm);
65-
66-
if (
67-
(hashSegments.length === 2 && hashSegments[0] === 'pm-with')
68-
|| (hashSegments.length === 4 && hashSegments[0] === 'pm-with' && hashSegments[2] === 'near')
69-
) {
70-
return 'pm';
71-
}
72-
73-
if (
74-
(hashSegments.length === 4 || hashSegments.length === 6)
75-
&& hashSegments[0] === 'stream'
76-
&& (hashSegments[2] === 'subject' || hashSegments[2] === 'topic')
77-
) {
78-
return 'topic';
79-
}
80-
81-
if (hashSegments.length === 2 && hashSegments[0] === 'stream') {
82-
return 'stream';
83-
}
84-
85-
if (
86-
hashSegments.length === 2
87-
&& hashSegments[0] === 'is'
88-
&& /^(private|starred|mentioned)/i.test(hashSegments[1])
89-
) {
90-
return 'special';
91-
}
92-
93-
return 'home';
94-
};
95-
9650
/** Decode a dot-encoded string. */
9751
// The Zulip webapp uses this encoding in narrow-links:
9852
// https://github.com/zulip/zulip/blob/1577662a6/static/js/hash_util.js#L18-L25
@@ -167,41 +121,49 @@ export const getNarrowFromLink = (
167121
return null;
168122
}
169123

170-
const type = getLinkType(url, realm);
171-
172124
// isNarrowLink(…) is true, by early return above, so this call is OK.
173125
const hashSegments = getHashSegmentsFromNarrowLink(url, realm);
174126

175-
switch (type) {
176-
case 'pm': {
177-
// TODO: This case is pretty useless in practice, due to basically a
178-
// bug in the webapp: the URL that appears in the location bar for a
179-
// group PM conversation excludes self, so it's unusable for anyone
180-
// else. In particular this will foil you if, say, you try to give
181-
// someone else in the conversation a link to a particular message.
182-
const ids = parsePmOperand(hashSegments[1]);
183-
return pmNarrowFromRecipients(pmKeyRecipientsFromIds(ids, ownUserId));
184-
}
185-
case 'topic': {
186-
const stream = parseStreamOperand(hashSegments[1], streamsById, streamsByName);
187-
return stream && topicNarrow(stream.stream_id, parseTopicOperand(hashSegments[3]));
188-
}
189-
case 'stream': {
190-
const stream = parseStreamOperand(hashSegments[1], streamsById, streamsByName);
191-
return stream && streamNarrow(stream.stream_id);
192-
}
193-
case 'special':
194-
try {
195-
return specialNarrow(hashSegments[1]);
196-
} catch {
197-
return null;
198-
}
199-
case 'home':
200-
return null; // TODO(?): Give HOME_NARROW
201-
default:
202-
ensureUnreachable(type);
127+
if (
128+
(hashSegments.length === 2 && hashSegments[0] === 'pm-with')
129+
|| (hashSegments.length === 4 && hashSegments[0] === 'pm-with' && hashSegments[2] === 'near')
130+
) {
131+
// TODO: This case is pretty useless in practice, due to basically a
132+
// bug in the webapp: the URL that appears in the location bar for a
133+
// group PM conversation excludes self, so it's unusable for anyone
134+
// else. In particular this will foil you if, say, you try to give
135+
// someone else in the conversation a link to a particular message.
136+
const ids = parsePmOperand(hashSegments[1]);
137+
return pmNarrowFromRecipients(pmKeyRecipientsFromIds(ids, ownUserId));
138+
}
139+
140+
if (
141+
(hashSegments.length === 4 || hashSegments.length === 6)
142+
&& hashSegments[0] === 'stream'
143+
&& (hashSegments[2] === 'subject' || hashSegments[2] === 'topic')
144+
) {
145+
const stream = parseStreamOperand(hashSegments[1], streamsById, streamsByName);
146+
return stream && topicNarrow(stream.stream_id, parseTopicOperand(hashSegments[3]));
147+
}
148+
149+
if (hashSegments.length === 2 && hashSegments[0] === 'stream') {
150+
const stream = parseStreamOperand(hashSegments[1], streamsById, streamsByName);
151+
return stream && streamNarrow(stream.stream_id);
152+
}
153+
154+
if (
155+
hashSegments.length === 2
156+
&& hashSegments[0] === 'is'
157+
&& /^(private|starred|mentioned)/i.test(hashSegments[1])
158+
) {
159+
try {
160+
return specialNarrow(hashSegments[1]);
161+
} catch {
203162
return null;
163+
}
204164
}
165+
166+
return null; // TODO(?) Give HOME_NARROW
205167
};
206168

207169
/**

0 commit comments

Comments
 (0)