-
Notifications
You must be signed in to change notification settings - Fork 309
compose: Prototype upload-file UI #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2b3e781
ef3db68
ab60489
ca20d0e
723b399
1650f0d
f9139e4
5dca335
e221d46
0c029b5
cb25643
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,9 +32,13 @@ abstract class ApiConnection { | |
// that ensures nothing assumes base class has a real API key | ||
final Auth auth; | ||
|
||
void close(); | ||
|
||
Future<String> get(String route, Map<String, dynamic>? params); | ||
|
||
Future<String> post(String route, Map<String, dynamic>? params); | ||
|
||
Future<String> postFileFromStream(String route, Stream<List<int>> content, int length, { String? filename }); | ||
} | ||
|
||
// TODO memoize | ||
|
@@ -49,10 +53,22 @@ Map<String, String> authHeader(Auth auth) { | |
class LiveApiConnection extends ApiConnection { | ||
LiveApiConnection({required super.auth}); | ||
|
||
final http.Client _client = http.Client(); | ||
|
||
bool _isOpen = true; | ||
|
||
@override | ||
void close() { | ||
assert(_isOpen); | ||
_client.close(); | ||
_isOpen = false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: this line should appear in the commit that adds |
||
} | ||
|
||
Map<String, String> _headers() => authHeader(auth); | ||
|
||
@override | ||
Future<String> get(String route, Map<String, dynamic>? params) async { | ||
assert(_isOpen); | ||
final baseUrl = Uri.parse(auth.realmUrl); | ||
final url = Uri( | ||
scheme: baseUrl.scheme, | ||
|
@@ -62,7 +78,7 @@ class LiveApiConnection extends ApiConnection { | |
path: "/api/v1/$route", | ||
queryParameters: encodeParameters(params)); | ||
if (kDebugMode) print("GET $url"); | ||
final response = await http.get(url, headers: _headers()); | ||
final response = await _client.get(url, headers: _headers()); | ||
if (response.statusCode != 200) { | ||
throw Exception("error on GET $route: status ${response.statusCode}"); | ||
} | ||
|
@@ -71,7 +87,8 @@ class LiveApiConnection extends ApiConnection { | |
|
||
@override | ||
Future<String> post(String route, Map<String, dynamic>? params) async { | ||
final response = await http.post( | ||
assert(_isOpen); | ||
final response = await _client.post( | ||
Uri.parse("${auth.realmUrl}/api/v1/$route"), | ||
headers: _headers(), | ||
body: encodeParameters(params)); | ||
|
@@ -80,6 +97,19 @@ class LiveApiConnection extends ApiConnection { | |
} | ||
return utf8.decode(response.bodyBytes); | ||
} | ||
|
||
@override | ||
Future<String> postFileFromStream(String route, Stream<List<int>> content, int length, { String? filename }) async { | ||
assert(_isOpen); | ||
http.MultipartRequest request = http.MultipartRequest('POST', Uri.parse("${auth.realmUrl}/api/v1/$route")) | ||
..files.add(http.MultipartFile('file', content, length, filename: filename)) | ||
..headers.addAll(_headers()); | ||
Comment on lines
+104
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Neat. Where did you find this as the way to make this sort of request? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed that
—and then I was also happy to see the convenient interface for uploading files, with For how to represent the file, I chose |
||
final response = await http.Response.fromStream(await _client.send(request)); | ||
if (response.statusCode != 200) { | ||
throw Exception("error on file-upload POST $route: status ${response.statusCode}"); | ||
} | ||
return utf8.decode(response.bodyBytes); | ||
} | ||
} | ||
|
||
Map<String, dynamic>? encodeParameters(Map<String, dynamic>? params) { | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -97,3 +97,28 @@ class SendMessageResult { | |
|
||
Map<String, dynamic> toJson() => _$SendMessageResultToJson(this); | ||
} | ||
|
||
/// https://zulip.com/api/upload-file | ||
Future<UploadFileResult> uploadFile( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit in commit message:
The form in previous commits would be:
In particular the explicitness that the name it's mentioning is the name of a route seems helpful. |
||
ApiConnection connection, { | ||
required Stream<List<int>> content, | ||
required int length, | ||
required String filename, | ||
}) async { | ||
final data = await connection.postFileFromStream('user_uploads', content, length, filename: filename); | ||
return UploadFileResult.fromJson(jsonDecode(data)); | ||
} | ||
|
||
@JsonSerializable(fieldRename: FieldRename.snake) | ||
class UploadFileResult { | ||
final String uri; | ||
|
||
UploadFileResult({ | ||
required this.uri, | ||
}); | ||
|
||
factory UploadFileResult.fromJson(Map<String, dynamic> json) => | ||
_$UploadFileResultFromJson(json); | ||
|
||
Map<String, dynamic> toJson() => _$UploadFileResultToJson(this); | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -142,13 +142,15 @@ class PerAccountStore extends ChangeNotifier { | |
required InitialSnapshot initialSnapshot, | ||
}) : zulipVersion = initialSnapshot.zulipVersion, | ||
subscriptions = Map.fromEntries(initialSnapshot.subscriptions.map( | ||
(subscription) => MapEntry(subscription.streamId, subscription))); | ||
(subscription) => MapEntry(subscription.streamId, subscription))), | ||
maxFileUploadSizeMib = initialSnapshot.maxFileUploadSizeMib; | ||
|
||
final Account account; | ||
final ApiConnection connection; | ||
|
||
final String zulipVersion; | ||
final Map<int, Subscription> subscriptions; | ||
final int maxFileUploadSizeMib; // No event for this. | ||
|
||
// TODO lots more data. When adding, be sure to update handleEvent too. | ||
|
||
|
@@ -294,7 +296,7 @@ class LivePerAccountStore extends PerAccountStore { | |
final result = await getEvents(connection, | ||
queueId: queueId, lastEventId: lastEventId); | ||
// TODO handle errors on get-events; retry with backoff | ||
// TODO abort long-poll on [dispose] | ||
// TODO abort long-poll and close LiveApiConnection on [dispose] | ||
Comment on lines
-297
to
+299
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: this too |
||
final events = result.events; | ||
for (final event in events) { | ||
handleEvent(event); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Attempting to close twice seems like that'd be a sign of a bug and we'd want to catch it with an assert.