Skip to content

Commit 1ad4aec

Browse files
committed
internal_link: Parse uploaded-file links
1 parent 74f424e commit 1ad4aec

File tree

3 files changed

+61
-0
lines changed

3 files changed

+61
-0
lines changed

lib/model/internal_link.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,36 @@ class NarrowLink extends InternalLink {
128128
final int? nearMessageId;
129129
}
130130

131+
/// A parsed link to an uploaded file in Zulip.
132+
///
133+
/// The structure mirrors the data required for [getFileTemporaryUrl]:
134+
/// https://zulip.com/api/get-file-temporary-url
135+
class UserUploadLink extends InternalLink {
136+
UserUploadLink(this.realmId, this.path, {required super.realmUrl});
137+
138+
static UserUploadLink? _tryParse(String urlPath, Uri realmUrl) {
139+
final match = _urlPathRegexp.matchAsPrefix(urlPath);
140+
if (match == null) return null;
141+
final realmId = int.parse(match.group(1)!, radix: 10);
142+
return UserUploadLink(realmId, match.group(2)!, realmUrl: realmUrl);
143+
}
144+
145+
static const _urlPathPrefix = '/user_uploads/';
146+
static final _urlPathRegexp = RegExp(r'^/user_uploads/(\d+)/(.+)$');
147+
148+
final int realmId;
149+
150+
/// The remaining path components after the realm ID.
151+
///
152+
/// This excludes the slash that separates the realm ID from the
153+
/// next component, but includes the rest of the URL path after that slash.
154+
///
155+
/// This corresponds to `filename` in the arguments to [getFileTemporaryUrl];
156+
/// but it's typically several path components,
157+
/// not just one as that name would suggest.
158+
final String path;
159+
}
160+
131161
/// Try to parse the given URL as a page in this app, on `store`'s realm.
132162
///
133163
/// `url` must already be a result from [PerAccountStore.tryResolveUrl]
@@ -161,6 +191,8 @@ InternalLink? parseInternalLink(Uri url, PerAccountStore store) {
161191
if (segments.isEmpty || !segments.length.isEven) return null;
162192
return _interpretNarrowSegments(segments, store);
163193
}
194+
} else if (url.path.startsWith(UserUploadLink._urlPathPrefix)) {
195+
return UserUploadLink._tryParse(url.path, store.realmUrl);
164196
}
165197

166198
return null;

lib/widgets/content.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,7 @@ void _launchUrl(BuildContext context, String urlString) async {
14361436
narrow: internalLink.narrow,
14371437
initAnchorMessageId: internalLink.nearMessageId)));
14381438

1439+
case UserUploadLink(): // TODO(#1732): handle these specially
14391440
case null:
14401441
await PlatformActions.launchUrl(context, url);
14411442
}

test/model/internal_link_test.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,34 @@ void main() {
586586
});
587587
});
588588
});
589+
590+
group('parseInternalLink uploads', () {
591+
for (final (expectParse, urlPath) in [
592+
(true, '/user_uploads/123/abc.pdf'),
593+
(true, '/user_uploads/123/4/ab/5/cde'),
594+
(false, '/user_uploads/123/'),
595+
(false, '/user_uploads/123'),
596+
(false, '/user_uploads/'),
597+
(false, '/user_uploads'),
598+
(false, '/user_uploads//abc.pdf'),
599+
(false, '/user_uploads/-123/abc.pdf'),
600+
(false, '/user_upload/123/abc.pdf'),
601+
]) {
602+
test('$urlPath -> ${expectParse ? 'parse' : 'reject'}', () {
603+
final store = eg.store();
604+
final url = store.tryResolveUrl(urlPath)!;
605+
final result = parseInternalLink(url, store);
606+
if (!expectParse) {
607+
check(result).isNull();
608+
} else {
609+
check(result).isA<UserUploadLink>();
610+
result as UserUploadLink;
611+
final reconstructedPath = '/user_uploads/${result.realmId}/${result.path}';
612+
check(reconstructedPath).equals(urlPath);
613+
}
614+
});
615+
}
616+
});
589617
}
590618

591619
extension InternalLinkChecks on Subject<InternalLink> {

0 commit comments

Comments
 (0)