@@ -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 ;
0 commit comments