Skip to content

Initial framework support for iOS platform views. - #23412

Merged
amirh merged 5 commits into
flutter:masterfrom
amirh:uikit_view
Oct 31, 2018
Merged

amirh merged 5 commits into
flutter:masterfrom
amirh:uikit_view

Conversation

@amirh

@amirh amirh commented Oct 23, 2018 •

Copy link
Copy Markdown
Contributor

This PR adds the full framework stack (layer->render object->widget, and
service) for embedding iOS views with minimal functionality (see below for what's still missing).

I allowed myself to throw the entire framework stack in one PR since we're mostly
mirroring the structure we already established for embedded Android views, so this PR
is a little bigger than usual. I'm happy to break it down to the
different pieces of the stack if reviewers prefer.

Specifically this PR adds:

  • A UiKitView widget for embedding a UIView in the widget tree.
  • A RenderUiKitView which is the render object for showing a
    UIView.
  • A PlatformViewLayer which denotes the position of a UIView in the
    layer tree.
  • The iOS platform_views system channel client code in services/platform_views.dart
  • Splits the fake platform views controller to an Android and iOS
    controllers.

TBD in following PRs:

  • Plumb the layout direction all the way to the platform view (currently
    there is still no engine support for it).
  • Integrate with gesture arenas (engine support is still missing as
    well).

#19030

This PR adds the full framework stack (layer->render object->widget, and
service) for embedding iOS views with minimal functionality.

I allowed myself to throw the entire framework stack in one PR since we're mostly
mirroring the structure we already established for embedded Android views, so this PR
is a little bigger than usual. I'm happy to break it down to the
different pieces of the stack if reviewers prefer.

Specifically this PR adds:
* A UiKitView widget for embedding a UIView in the widget tree.
* A RenderUiKitView which is the render object for showing a
  UIView.
* A PlatformViewLayer which denotes the position of a UIView in the
  layer tree.
* The iOS platform_views system channel client code in services/platform_views.dart
* Splits the fake platform views controller to an Android and iOS
  controllers.

TBD in following PRs:
* Plumb the layout direction all the way to the platform view (currently
  there is still no engine support for it).
* Integrate with gesture arenas (engine support is still missing as
  well).
@amirh
amirh requested a review from xster October 23, 2018 17:13
/// [RenderAndroidView] is responsible for sizing, displaying and passing touch events to an
/// Android [View](https://developer.android.com/reference/android/view/View).
///
/// {@template platform_render_layout}

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.

Be more specific about namespacing. See other usages of @template

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done


/// A layer that shows an embedded [UIView](https://developer.apple.com/documentation/uikit/uiview)
/// on iOS.
class PlatformViewLayer extends Layer {

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.

Are we ever going to give a view id to fuchsia and non-iOS to compose? I worry that we'll add actual iOS specific stuff to this class later on and dilute the 'PlatformView' nomenclature since we've been fairly good at calling Android things RenderAndroidView etc.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I can imagine this getting used for non-iOS use-cases.
Also note that in the engine we abstracted by the platform-agnostic Surface.

@required this.viewId,
}): assert(rect != null), assert(viewId != null);

/// Bounding rectangle of this layer.

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.

From whose perspective?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Mentioned it's in the global coordinate space, hope that clarifies?

///
/// [RenderUiKitView] is responsible for sizing and displaying an iOS
/// [UIView](https://developer.apple.com/documentation/uikit/uiview).
///

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.

Add more details about hit testing and how it delegates painting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a note about how the UIViews are composited (this is still churning so I'm purposely keeping it pretty abstract here).

Hit testing is explained on PlatformViewHitTestBehavior, skimming through proxy_box.dart I see that this is the common practice (e.g we don't normally describe hit testing behavior in the render objects class docs).

Right now touch support is not yet implemented, I'm planning to add some docs about it when adding the support.

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.

SG

_viewId = viewId;


/// The unique identifier of the UIView controlled by this controller.

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.

Cross reference platform view services here again?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

bool _initialized = false;

static final Set<Factory<OneSequenceGestureRecognizer>> _emptyRecognizersSet =
Set<Factory<OneSequenceGestureRecognizer>>();

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.

indent

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

@override
Widget build(BuildContext context) {
if (_controller == null) {
return const SizedBox.expand();

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.

Should we allow for a custom specifiable placeholder for android and uikitview?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's probably useful, though I'd keep it for a future PR, this one is already doing too many things.

return;
}
_initialized = true;
_layoutDirection = _findLayoutDirection();

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.

I don't think you need this one. Since didChangeDependencies is always called and it always does this anyway.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We need _layoutDirection set before calling _createNewUiKitView for the fist time...

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.

Still feel like there's some redundancy. Maybe move lines 339-341 2 lines up?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good suggestion, done, it's cleaner now.

if (widget.onPlatformViewCreated != null) {
widget.onPlatformViewCreated(_id);
}
setState(() {});

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.

final local controller and setState(() { _controller = controller; }); just to be explicit? Or at least add some docs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

}
}

class _UiKitPlatformView extends LeafRenderObjectWidget {

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.

I think the convention is calling these XRenderObjectWidget (if you global search for extends LeafRenderObjectWidget)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Doesn't seem to be a convention...

$ git grep "extends LeafRenderObject" | wc -l
      23
$ git grep "RenderObjectWidget extends LeafRenderObject" | wc -l
       6
$ git grep "extends LeafRenderObject"
... 
lib/src/widgets/basic.dart:class RichText extends LeafRenderObjectWidget {
lib/src/widgets/basic.dart:class RawImage extends LeafRenderObjectWidget {
lib/src/widgets/basic.dart:class WidgetToRenderBoxAdapter extends LeafRenderObjectWidget {
lib/src/widgets/editable_text.dart:class _Editable extends LeafRenderObjectWidget {
lib/src/widgets/framework.dart:class ErrorWidget extends LeafRenderObjectWidget {
lib/src/widgets/performance_overlay.dart:class PerformanceOverlay extends LeafRenderObjectWidget {
...

/// Sets the layout direction for the Android view.
Future<void> setLayoutDirection(TextDirection layoutDirection) async {
if (_disposed)
throw FlutterError('trying to set a layout direction for a disposed Android View. View id: $id');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i would only check this in debug mode

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

/// The unique identifier of the iOS view controlled by this controller.
final int id;

bool _disposed = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

i would call this _debugDisposed and only track it in debug mode

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

///
/// {@macro platform_view_constructor_params}
UiKitView({ // ignore: prefer_const_constructors_in_immutables
// TODO(aam): Remove lint ignore above once dartbug.com/34297 is fixed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can you make that link a full link (https://... etc)? That way it comes up in searches for links when I do audits. Thanks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

final TextDirection layoutDirection;

@override
State createState() => _UiKitViewState();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

State<UiKitView> or _UiKitViewState, not just State.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

viewType: widget.viewType,
layoutDirection: _layoutDirection,
);
if (widget.onPlatformViewCreated != null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

since this is async, you might get disposed before initUiKitView returns. You should check mounted is true here and return early if it is not.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch!
Fixed and added a unit test.

if (widget.onPlatformViewCreated != null) {
widget.onPlatformViewCreated(_id);
}
setState(() {});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

always put a comment in empty setState blocks explaining what state changed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

changed to be non empty setState.


@override
void dispose() {
_controller.dispose();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

_controller might be null here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fixed (and tested)

_layoutDirection = newLayoutDirection;

if (widget.viewType != oldWidget.viewType) {
_controller.dispose();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

_controller might be null here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

@amirh amirh left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks @xster ! PTAL

/// apply to it as well.
///
/// The widget fill all available space, the parent of this object must provide bounded layout
/// {@template platform_view_layout}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

/// ```
///
/// The Android view's lifetime is the same as the lifetime of the [State] object for this widget.
/// {@template platform_view_lifetime}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done


/// The codec used to encode `creationParams` before sending it to the
/// platform side. It should match the codec passed to the constructor of [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#PlatformViewFactory-io.flutter.plugin.common.MessageCodec-).
/// platform side.It should match the codec passed to the constructor of [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#PlatformViewFactory-io.flutter.plugin.common.MessageCodec-).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

bool _initialized = false;

static final Set<Factory<OneSequenceGestureRecognizer>> _emptyRecognizersSet =
Set<Factory<OneSequenceGestureRecognizer>>();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

@override
Widget build(BuildContext context) {
if (_controller == null) {
return const SizedBox.expand();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

That's probably useful, though I'd keep it for a future PR, this one is already doing too many things.

TextDirection layoutDirection,
) : assert(id != null),
assert(layoutDirection != null),
_layoutDirection = layoutDirection;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The layoutDirection will be passed as an argument of the create call (this is not yet implemented in the engine).

_layoutDirection = layoutDirection;


/// The unique identifier of the iOS view controlled by this controller.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

/// Sets the layout direction for the Android view.
Future<void> setLayoutDirection(TextDirection layoutDirection) async {
if (_disposed)
throw FlutterError('trying to set a layout direction for a disposed Android View. View id: $id');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

/// Disposes the view.
///
/// The [UiKitViewController] object is unusable after calling this.
/// The identifier of the platform view cannot be reused after the view is

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

///
/// Embedding iOS views is an expensive operation and should be avoided when a Flutter
/// equivalent is possible.
///

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a TODO, there's going to be some churn in the way it works with the incremental progress on the engine side, I'll describe it here once it settled.

}

if (didChangeLayoutDirection) {
_controller.setLayoutDirection(_layoutDirection);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

_controller might be null here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

if (didChangeLayoutDirection) {
// The native view will update asynchronously, in the meantime we don't want
// to block the framework. (so this is intentionally not awaiting).
_controller.setLayoutDirection(_layoutDirection);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

_controller might be null here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

RenderUiKitView(
viewId: viewId,
hitTestBehavior: hitTestBehavior,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: avoid line breaks in => expressions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

return false;
final FakeIosPlatformView typedOther = other;
return id == typedOther.id &&
type == typedOther.type;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: indent

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

}
}

class FakeIosPlatformView {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/me peers suspiciously at the "Ios"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed to FakeUiKitView


@override
bool operator ==(dynamic other) {
if (other is! FakeIosPlatformView)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

compare runtimeType instead of using is in operator==, otherwise subclasses will have asymmetric == operators

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done (here and in FakeAndroidPlatformView)


expect(factoryInvocationCount, 1);
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

consider adding tests that hit the "_controller is null" side of everywhere that you end up testing whether _controller is null (doing so will involve creating a bunch of interesting tests that test edge cases of things changing at various times, disposing early, etc, which is generally valuable)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

added a 'disposeUIView before creation completed' test case

@amirh

amirh commented Oct 24, 2018

Copy link
Copy Markdown
Contributor Author

Thanks @Hixie ! PTAL

@xster xster left a comment

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.

LGTM

return;
}
_initialized = true;
_layoutDirection = _findLayoutDirection();

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.

Still feel like there's some redundancy. Maybe move lines 339-341 2 lines up?

}

class FakeAndroidPlatformView {

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.

nit: symmetrical white spaces

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

}

class FakeUiKitView {

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.

ditto

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

layoutDirection: TextDirection.ltr,
);
expect(
() =>

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.

some formatting issues here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

reformatted with dartfmt. (and it's weird)

unorderedEquals(<FakeUiKitView>[
FakeUiKitView(0, 'webview'),
FakeUiKitView(1, 'webview'),
]));

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.

trailing commas might get the formatter to return something cleaner

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

@amirh amirh left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks, I'll wait for some relevant engine stuff to land before landing this.

return;
}
_initialized = true;
_layoutDirection = _findLayoutDirection();

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good suggestion, done, it's cleaner now.

}

class FakeAndroidPlatformView {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

}

class FakeUiKitView {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

layoutDirection: TextDirection.ltr,
);
expect(
() =>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

reformatted with dartfmt. (and it's weird)

unorderedEquals(<FakeUiKitView>[
FakeUiKitView(0, 'webview'),
FakeUiKitView(1, 'webview'),
]));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done

@amirh
amirh merged commit 67ffe1c into flutter:master Oct 31, 2018
@amirh
amirh deleted the uikit_view branch October 31, 2018 13:01
amirh added a commit that referenced this pull request Oct 31, 2018
amirh added a commit that referenced this pull request Oct 31, 2018
amirh added a commit that referenced this pull request Oct 31, 2018
amirh added a commit that referenced this pull request Oct 31, 2018
Re-landing #23412 with a fix to the PlatformLayer's addToScene signature.

#23412 was broken by the change to Layer done in #23434.

It seemed green as the presubmits were done before #23434 was landed, and when #23412 landed it broke the build.

Reverts #23779
Xavjer pushed a commit to Xavjer/flutter that referenced this pull request Nov 1, 2018
This PR adds the full framework stack (layer->render object->widget, and
service) for embedding iOS views with minimal functionality.

I allowed myself to throw the entire framework stack in one PR since we're mostly
mirroring the structure we already established for embedded Android views, so this PR
is a little bigger than usual. I'm happy to break it down to the
different pieces of the stack if reviewers prefer.

Specifically this PR adds:
* A UiKitView widget for embedding a UIView in the widget tree.
* A RenderUiKitView which is the render object for showing a
  UIView.
* A PlatformViewLayer which denotes the position of a UIView in the
  layer tree.
* The iOS platform_views system channel client code in services/platform_views.dart
* Splits the fake platform views controller to an Android and iOS
  controllers.

TBD in following PRs:
* Plumb the layout direction all the way to the platform view (currently
  there is still no engine support for it).
* Integrate with gesture arenas (engine support is still missing as
  well).
Xavjer pushed a commit to Xavjer/flutter that referenced this pull request Nov 1, 2018
Xavjer pushed a commit to Xavjer/flutter that referenced this pull request Nov 1, 2018
…23781)

Re-landing flutter#23412 with a fix to the PlatformLayer's addToScene signature.

flutter#23412 was broken by the change to Layer done in flutter#23434.

It seemed green as the presubmits were done before flutter#23434 was landed, and when flutter#23412 landed it broke the build.

Reverts flutter#23779
@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Aug 9, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants