Commit 252307c8 authored by Tim Song's avatar Tim Song Committed by Commit Bot

Reland "Ash Tray: Refactor slide out animation to re-use transform animation."

This is a reland of fc7ae248

Original change's description:
> Ash Tray: Refactor slide out animation to re-use transform animation.
> 
> There are currently two code paths to slide out a notification in the message
> center, which this CL rationalize this logic to just use the transform-based
> animation.
> 
> Furthermore, to implement the spec'd clear all animation, each MessageView
> needs to be animated separately, which isn't currently possible.
> 
> Note: the clear all animation still uses the old Layout() based slide-out
> animation. This will be removed once the spec'd clear all is implemented.
> 
> TEST=verified existing behaviour with normal and ARC++ notifications,
>      updated unit test
> BUG=897915
> 
> Change-Id: I294f4fe326730cf40a447c23d551e7d33838a579
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1613919
> Commit-Queue: Tim Song <tengs@chromium.org>
> Reviewed-by: Tetsui Ohkubo <tetsui@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#661553}

Bug: 897915
Change-Id: Ice73af1f3982d74f6b9c3006235165c52a0f4c7d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1621539Reviewed-by: default avatarTetsui Ohkubo <tetsui@chromium.org>
Commit-Queue: Tim Song <tengs@chromium.org>
Cr-Commit-Position: refs/heads/master@{#662266}
parent 34e29379
......@@ -15,6 +15,7 @@
#include "ash/system/unified/unified_system_tray_model.h"
#include "ash/test/ash_test_base.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
......@@ -80,6 +81,7 @@ class UnifiedMessageCenterViewTest : public AshTestBase,
}
void TearDown() override {
base::RunLoop().RunUntilIdle();
message_center_view_.reset();
model_.reset();
AshTestBase::TearDown();
......@@ -126,13 +128,19 @@ class UnifiedMessageCenterViewTest : public AshTestBase,
void AnimateMessageListToMiddle() { AnimateMessageListToValue(0.5); }
void AnimateMessageListToEnd() { GetMessageListView()->animation_->End(); }
void AnimateMessageListToEnd() {
FinishMessageListSlideOutAnimations();
GetMessageListView()->animation_->End();
}
void AnimateMessageListUntilIdle() {
while (GetMessageListView()->animation_->is_animating())
while (GetMessageListView()->animation_->is_animating()) {
GetMessageListView()->animation_->End();
}
}
void FinishMessageListSlideOutAnimations() { base::RunLoop().RunUntilIdle(); }
gfx::Rect GetMessageViewVisibleBounds(size_t index) {
gfx::Rect bounds = GetMessageListView()->children()[index]->bounds();
bounds -= GetScroller()->GetVisibleRect().OffsetFromOrigin();
......@@ -236,7 +244,7 @@ TEST_F(UnifiedMessageCenterViewTest, AddAndRemoveNotification) {
auto* collapse_animation = GetMessageCenterAnimation();
collapse_animation->SetCurrentValue(0.5);
message_center_view()->AnimationProgressed(collapse_animation);
AnimateMessageListToMiddle();
AnimateMessageListToEnd();
EXPECT_TRUE(message_center_view()->GetVisible());
// The message center is now hidden after all animations complete.
......@@ -263,12 +271,7 @@ TEST_F(UnifiedMessageCenterViewTest, RemoveNotificationAtTail) {
// Remove the last notification.
MessageCenter::Get()->RemoveNotification(id_to_remove, true /* by_user */);
// The first animation slides the notification out of the list, and the second
// animation collapses the list.
AnimateMessageListToEnd();
AnimateMessageListToValue(0);
// The scroll position should not change after sliding the notification out
// The scroll position should not change before sliding the notification out
// and instead should wait until the animation finishes.
EXPECT_EQ(scroll_position, GetScroller()->GetVisibleRect().y());
......@@ -297,7 +300,7 @@ TEST_F(UnifiedMessageCenterViewTest, ContentsRelayout) {
const int previous_list_height = GetMessageListView()->height();
MessageCenter::Get()->RemoveNotification(ids.back(), true /* by_user */);
AnimateMessageListUntilIdle();
AnimateMessageListToEnd();
EXPECT_TRUE(message_center_view()->GetVisible());
EXPECT_GT(previous_contents_height, GetScrollerContents()->height());
EXPECT_GT(previous_list_height, GetMessageListView()->height());
......@@ -535,7 +538,7 @@ TEST_F(UnifiedMessageCenterViewTest, StackingCounterRemovedWithNotifications) {
EXPECT_TRUE(GetStackingCounter()->GetVisible());
for (size_t i = 0; i < 5; ++i) {
MessageCenter::Get()->RemoveNotification(ids[i], true /* by_user */);
AnimateMessageListUntilIdle();
AnimateMessageListToEnd();
}
EXPECT_FALSE(GetStackingCounter()->GetVisible());
}
......@@ -629,7 +632,7 @@ TEST_F(UnifiedMessageCenterViewTest,
EXPECT_TRUE(GetStackingCounter()->GetVisible());
for (size_t i = 0; i < 4; ++i) {
MessageCenter::Get()->RemoveNotification(ids[i], true /* by_user */);
AnimateMessageListUntilIdle();
AnimateMessageListToEnd();
}
EXPECT_TRUE(GetStackingCounter()->GetVisible());
EXPECT_FALSE(GetStackingCounterLabel()->GetVisible());
......@@ -808,7 +811,7 @@ TEST_F(UnifiedMessageCenterViewTest, FocusClearedAfterNotificationRemoval) {
// Remove the notification and observe that the focus is cleared.
MessageCenter::Get()->RemoveNotification(id1, true /* by_user */);
AnimateMessageListUntilIdle();
AnimateMessageListToEnd();
EXPECT_FALSE(message_center_view()->GetFocusManager()->GetFocusedView());
widget->GetRootView()->RemoveChildView(message_center_view());
......
......@@ -29,7 +29,7 @@ namespace ash {
namespace {
constexpr base::TimeDelta kClosingAnimationDuration =
base::TimeDelta::FromMilliseconds(330);
base::TimeDelta::FromMilliseconds(320);
constexpr base::TimeDelta kClearAllStackedAnimationDuration =
base::TimeDelta::FromMilliseconds(40);
constexpr base::TimeDelta kClearAllVisibleAnimationDuration =
......@@ -69,8 +69,10 @@ class UnifiedMessageListView::MessageViewContainer
: public views::View,
public MessageView::SlideObserver {
public:
explicit MessageViewContainer(MessageView* message_view)
MessageViewContainer(MessageView* message_view,
UnifiedMessageListView* list_view)
: message_view_(message_view),
list_view_(list_view),
control_view_(new NotificationSwipeControlView(message_view)) {
message_view_->AddSlideObserver(this);
......@@ -79,7 +81,7 @@ class UnifiedMessageListView::MessageViewContainer
AddChildView(message_view_);
}
~MessageViewContainer() override = default;
~MessageViewContainer() override { message_view_->RemoveSlideObserver(this); }
// Update the border and background corners based on if the notification is
// at the top or the bottom.
......@@ -125,6 +127,10 @@ class UnifiedMessageListView::MessageViewContainer
}
}
void SlideOutAndClose() {
message_view_->SlideOutAndClose(1 /* direction */);
}
std::string GetNotificationId() const {
return message_view_->notification_id();
}
......@@ -160,7 +166,9 @@ class UnifiedMessageListView::MessageViewContainer
}
void OnSlideOut(const std::string& notification_id) override {
is_slid_out_by_user_ = true;
is_slid_out_ = true;
set_is_removed();
list_view_->OnNotificationSlidOut();
}
gfx::Rect start_bounds() const { return start_bounds_; }
......@@ -177,7 +185,7 @@ class UnifiedMessageListView::MessageViewContainer
void set_is_removed() { is_removed_ = true; }
bool is_slid_out_by_user() { return is_slid_out_by_user_; }
bool is_slid_out() { return is_slid_out_; }
private:
// The bounds that the container starts animating from. If not animating, it's
......@@ -188,14 +196,14 @@ class UnifiedMessageListView::MessageViewContainer
// actual bounds().
gfx::Rect ideal_bounds_;
// True when the notification is removed and during SLIDE_OUT animation.
// Unused if |state_| is not SLIDE_OUT.
// True when the notification is removed and during slide out animation.
bool is_removed_ = false;
// True if the notification is slid out completely by the user.
bool is_slid_out_by_user_ = false;
// True if the notification is slid out completely.
bool is_slid_out_ = false;
MessageView* const message_view_;
UnifiedMessageListView* const list_view_;
NotificationSwipeControlView* const control_view_;
DISALLOW_COPY_AND_ASSIGN(MessageViewContainer);
......@@ -224,7 +232,8 @@ UnifiedMessageListView::~UnifiedMessageListView() {
void UnifiedMessageListView::Init() {
bool is_latest = true;
for (auto* notification : MessageCenter::Get()->GetVisibleNotifications()) {
auto* view = new MessageViewContainer(CreateMessageView(*notification));
auto* view =
new MessageViewContainer(CreateMessageView(*notification), this);
view->LoadExpandedState(model_, is_latest);
AddChildViewAt(view, 0);
MessageCenter::Get()->DisplayedNotification(
......@@ -333,7 +342,7 @@ void UnifiedMessageListView::OnNotificationAdded(const std::string& id) {
auto* view = CreateMessageView(*notification);
// Expand the latest notification.
view->SetExpanded(view->IsAutoExpandingAllowed());
AddChildView(new MessageViewContainer(view));
AddChildView(new MessageViewContainer(view, this));
UpdateBorders();
ResetBounds();
}
......@@ -342,18 +351,32 @@ void UnifiedMessageListView::OnNotificationRemoved(const std::string& id,
bool by_user) {
if (ignore_notification_remove_)
return;
// The corresponding MessageView may have already been deleted after being
// manually slid out.
auto* child = GetNotificationById(id);
if (!child)
return;
InterruptClearAll();
ResetBounds();
auto* child = GetNotificationById(id);
if (child)
child->set_is_removed();
child->set_is_removed();
state_ = child->is_slid_out_by_user() ? State::MOVE_DOWN : State::SLIDE_OUT;
// If the MessageView is slid out, then do nothing here. The MOVE_DOWN
// animation will be started in OnNotificationSlidOut().
if (!child->is_slid_out())
child->SlideOutAndClose();
}
if (child->is_slid_out_by_user())
DeleteRemovedNotifications();
void UnifiedMessageListView::OnNotificationSlidOut() {
DeleteRemovedNotifications();
// |message_center_view_| can be null in tests.
if (message_center_view_)
message_center_view_->OnNotificationSlidOut();
state_ = State::MOVE_DOWN;
UpdateBounds();
StartAnimation();
}
......@@ -365,10 +388,13 @@ void UnifiedMessageListView::OnNotificationUpdated(const std::string& id) {
InterruptClearAll();
// The corresponding MessageView may have been slid out and deleted, so just
// ignore this update as the notification will soon be deleted.
auto* child = GetNotificationById(id);
if (child)
child->UpdateWithNotification(*notification);
if (!child)
return;
child->UpdateWithNotification(*notification);
ResetBounds();
}
......@@ -388,12 +414,7 @@ void UnifiedMessageListView::AnimationEnded(const gfx::Animation* animation) {
animation_->SetCurrentValue(1.0);
PreferredSizeChanged();
if (state_ == State::SLIDE_OUT) {
DeleteRemovedNotifications();
UpdateBounds();
state_ = State::MOVE_DOWN;
} else if (state_ == State::MOVE_DOWN) {
if (state_ == State::MOVE_DOWN) {
state_ = State::IDLE;
} else if (state_ == State::CLEAR_ALL_STACKED ||
state_ == State::CLEAR_ALL_VISIBLE) {
......@@ -541,14 +562,7 @@ void UnifiedMessageListView::StartAnimation() {
switch (state_) {
case State::IDLE:
break;
case State::SLIDE_OUT:
animation_->SetDuration(kClosingAnimationDuration);
animation_->Start();
break;
case State::MOVE_DOWN:
// |message_center_view_| can be null in tests.
if (message_center_view_)
message_center_view_->OnNotificationSlidOut();
animation_->SetDuration(kClosingAnimationDuration);
animation_->Start();
break;
......@@ -600,11 +614,10 @@ void UnifiedMessageListView::UpdateClearAllAnimation() {
}
double UnifiedMessageListView::GetCurrentValue() const {
return gfx::Tween::CalculateValue(
state_ == State::SLIDE_OUT || state_ == State::CLEAR_ALL_VISIBLE
? gfx::Tween::EASE_IN
: gfx::Tween::FAST_OUT_SLOW_IN,
animation_->GetCurrentValue());
return gfx::Tween::CalculateValue(state_ == State::CLEAR_ALL_VISIBLE
? gfx::Tween::EASE_IN
: gfx::Tween::FAST_OUT_SLOW_IN,
animation_->GetCurrentValue());
}
} // namespace ash
......@@ -68,6 +68,10 @@ class ASH_EXPORT UnifiedMessageListView
// Returns true if an animation is currently in progress.
bool IsAnimating() const;
// Called when a notification is slid out so we can run the MOVE_DOWN
// animation.
void OnNotificationSlidOut();
// views::View:
void ChildPreferredSizeChanged(views::View* child) override;
void PreferredSizeChanged() override;
......@@ -113,9 +117,6 @@ class ASH_EXPORT UnifiedMessageListView
// No animation is running.
IDLE,
// Sliding out a removed notification. It will transition to MOVE_DOWN.
SLIDE_OUT,
// Moving down notifications.
MOVE_DOWN,
......
......@@ -8,9 +8,11 @@
#include "ash/system/tray/tray_constants.h"
#include "ash/system/unified/unified_system_tray_model.h"
#include "ash/test/ash_test_base.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "ui/compositor/layer_animator.h"
#include "ui/message_center/message_center.h"
#include "ui/message_center/views/message_view.h"
#include "ui/message_center/views/notification_view_md.h"
......@@ -26,7 +28,11 @@ namespace {
class TestNotificationView : public message_center::NotificationViewMD {
public:
TestNotificationView(const message_center::Notification& notification)
: NotificationViewMD(notification) {}
: NotificationViewMD(notification) {
layer()->GetAnimator()->set_preemption_strategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
}
~TestNotificationView() override = default;
// message_center::NotificationViewMD:
......@@ -139,6 +145,8 @@ class UnifiedMessageListViewTest : public AshTestBase,
return message_list_view()->children()[index]->bounds();
}
void FinishSlideOutAnimation() { base::RunLoop().RunUntilIdle(); }
void AnimateToMiddle() {
EXPECT_TRUE(IsAnimating());
message_list_view()->animation_->SetCurrentValue(0.5);
......@@ -164,6 +172,10 @@ class UnifiedMessageListViewTest : public AshTestBase,
int size_changed_count() const { return size_changed_count_; }
ui::LayerAnimator* LayerAnimatorAt(int i) {
return GetMessageViewAt(i)->layer()->GetAnimator();
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
int id_ = 0;
......@@ -246,13 +258,15 @@ TEST_F(UnifiedMessageListViewTest, RemoveNotification) {
CreateMessageListView();
int previous_height = message_list_view()->GetPreferredSize().height();
EXPECT_EQ(2u, message_list_view()->children().size());
EXPECT_EQ(kUnifiedTrayCornerRadius, GetMessageViewAt(0)->top_radius());
EXPECT_EQ(0, GetMessageViewAt(0)->bottom_radius());
gfx::Rect previous_bounds = GetMessageViewBounds(0);
MessageCenter::Get()->RemoveNotification(id0, true /* by_user */);
FinishSlideOutAnimation();
AnimateUntilIdle();
EXPECT_EQ(3, size_changed_count());
EXPECT_EQ(1u, message_list_view()->children().size());
EXPECT_EQ(previous_bounds.y(), GetMessageViewBounds(0).y());
EXPECT_LT(0, message_list_view()->GetPreferredSize().height());
EXPECT_GT(previous_height, message_list_view()->GetPreferredSize().height());
......@@ -261,8 +275,9 @@ TEST_F(UnifiedMessageListViewTest, RemoveNotification) {
EXPECT_EQ(kUnifiedTrayCornerRadius, GetMessageViewAt(0)->bottom_radius());
MessageCenter::Get()->RemoveNotification(id1, true /* by_user */);
FinishSlideOutAnimation();
AnimateUntilIdle();
EXPECT_EQ(6, size_changed_count());
EXPECT_EQ(0u, message_list_view()->children().size());
EXPECT_EQ(0, message_list_view()->GetPreferredSize().height());
}
......@@ -300,11 +315,7 @@ TEST_F(UnifiedMessageListViewTest, RemovingNotificationAnimation) {
gfx::Rect bounds1 = GetMessageViewBounds(1);
MessageCenter::Get()->RemoveNotification(id1, true /* by_user */);
AnimateToMiddle();
gfx::Rect slided_bounds = GetMessageViewBounds(1);
EXPECT_LT(bounds1.x(), slided_bounds.x());
AnimateToEnd();
FinishSlideOutAnimation();
AnimateToMiddle();
EXPECT_GT(previous_height, message_list_view()->GetPreferredSize().height());
previous_height = message_list_view()->GetPreferredSize().height();
......@@ -315,7 +326,7 @@ TEST_F(UnifiedMessageListViewTest, RemovingNotificationAnimation) {
EXPECT_EQ(bounds1, GetMessageViewBounds(1));
MessageCenter::Get()->RemoveNotification(id2, true /* by_user */);
AnimateToEnd();
FinishSlideOutAnimation();
AnimateToMiddle();
EXPECT_GT(previous_height, message_list_view()->GetPreferredSize().height());
previous_height = message_list_view()->GetPreferredSize().height();
......@@ -325,7 +336,7 @@ TEST_F(UnifiedMessageListViewTest, RemovingNotificationAnimation) {
EXPECT_EQ(bounds0, GetMessageViewBounds(0));
MessageCenter::Get()->RemoveNotification(id0, true /* by_user */);
AnimateToEnd();
FinishSlideOutAnimation();
AnimateToMiddle();
EXPECT_GT(previous_height, message_list_view()->GetPreferredSize().height());
previous_height = message_list_view()->GetPreferredSize().height();
......@@ -340,6 +351,7 @@ TEST_F(UnifiedMessageListViewTest, ResetAnimation) {
CreateMessageListView();
MessageCenter::Get()->RemoveNotification(id0, true /* by_user */);
FinishSlideOutAnimation();
EXPECT_TRUE(IsAnimating());
AnimateToMiddle();
......@@ -520,7 +532,7 @@ TEST_F(UnifiedMessageListViewTest, ClearAllWithPinnedNotifications) {
TEST_F(UnifiedMessageListViewTest, UserSwipesAwayNotification) {
// Show message list with two notifications.
AddNotification();
AddNotification();
auto id1 = AddNotification();
CreateMessageListView();
// Start swiping the notification away.
......@@ -531,7 +543,8 @@ TEST_F(UnifiedMessageListViewTest, UserSwipesAwayNotification) {
// Swiping away the notification should remove it both in the MessageCenter
// and the MessageListView.
GetMessageViewAt(1)->OnSlideOut();
MessageCenter::Get()->RemoveNotification(id1, true /* by_user */);
FinishSlideOutAnimation();
EXPECT_EQ(1u, MessageCenter::Get()->GetVisibleNotifications().size());
EXPECT_EQ(1u, message_list_view()->children().size());
......
......@@ -130,6 +130,10 @@ void MessageView::CloseSwipeControl() {
slide_out_controller_.CloseSwipeControl();
}
void MessageView::SlideOutAndClose(int direction) {
slide_out_controller_.SlideOutAndClose(direction);
}
void MessageView::SetExpanded(bool expanded) {
// Not implemented by default.
}
......@@ -304,27 +308,31 @@ ui::Layer* MessageView::GetSlideOutLayer() {
}
void MessageView::OnSlideStarted() {
for (auto* observer : slide_observers_) {
observer->OnSlideStarted(notification_id_);
for (auto& observer : slide_observers_) {
observer.OnSlideStarted(notification_id_);
}
}
void MessageView::OnSlideChanged(bool in_progress) {
for (auto* observer : slide_observers_) {
observer->OnSlideChanged(notification_id_);
for (auto& observer : slide_observers_) {
observer.OnSlideChanged(notification_id_);
}
}
void MessageView::AddSlideObserver(MessageView::SlideObserver* observer) {
slide_observers_.push_back(observer);
slide_observers_.AddObserver(observer);
}
void MessageView::OnSlideOut() {
for (auto* observer : slide_observers_)
observer->OnSlideOut(notification_id_);
void MessageView::RemoveSlideObserver(MessageView::SlideObserver* observer) {
slide_observers_.RemoveObserver(observer);
}
void MessageView::OnSlideOut() {
MessageCenter::Get()->RemoveNotification(notification_id_,
true /* by_user */);
for (auto& observer : slide_observers_)
observer.OnSlideOut(notification_id_);
}
void MessageView::OnWillChangeFocus(views::View* before, views::View* now) {}
......
......@@ -8,6 +8,7 @@
#include <memory>
#include "base/macros.h"
#include "base/observer_list.h"
#include "base/strings/string16.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
......@@ -92,6 +93,7 @@ class MESSAGE_CENTER_EXPORT MessageView : public views::InkDropHostView,
virtual bool IsManuallyExpandedOrCollapsed() const;
virtual void SetManuallyExpandedOrCollapsed(bool value);
virtual void CloseSwipeControl();
virtual void SlideOutAndClose(int direction);
// Update corner radii of the notification. Subclasses will override this to
// implement rounded corners if they don't use MessageView's default
......@@ -136,6 +138,7 @@ class MESSAGE_CENTER_EXPORT MessageView : public views::InkDropHostView,
void OnDidChangeFocus(views::View* before, views::View* now) override;
void AddSlideObserver(SlideObserver* observer);
void RemoveSlideObserver(SlideObserver* observer);
Mode GetMode() const;
......@@ -190,7 +193,7 @@ class MESSAGE_CENTER_EXPORT MessageView : public views::InkDropHostView,
std::unique_ptr<views::Painter> focus_painter_;
SlideOutController slide_out_controller_;
std::vector<SlideObserver*> slide_observers_;
base::ObserverList<SlideObserver>::Unchecked slide_observers_;
// True if |this| is embedded in another view. Equivalent to |!top_level| in
// MessageViewFactory parlance.
......
......@@ -15,6 +15,7 @@ namespace message_center {
namespace {
constexpr int kSwipeRestoreDurationMs = 150;
constexpr int kSwipeOutTotalDurationMs = 150;
gfx::Tween::Type kSwipeTweenType = gfx::Tween::EASE_IN;
} // anonymous namespace
SlideOutController::SlideOutController(ui::EventTarget* target,
......@@ -171,6 +172,7 @@ void SlideOutController::SetTransformWithAnimationIfNecessary(
if (layer->transform() != transform) {
ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
settings.SetTransitionDuration(animation_duration);
settings.SetTweenType(kSwipeTweenType);
settings.AddObserver(this);
// An animation starts. OnImplicitAnimationsCompleted will be called just
......
......@@ -70,6 +70,10 @@ class MESSAGE_CENTER_EXPORT SlideOutController
// Effective only when swipe control is enabled by EnableSwipeControl().
void CloseSwipeControl();
// Slides the view out and closes it after the animation. The sign of
// |direction| indicates which way the slide occurs.
void SlideOutAndClose(int direction);
private:
// Positions where the slided view stays after the touch released.
enum class SwipeControlOpenState { CLOSED, OPEN_ON_LEFT, OPEN_ON_RIGHT };
......@@ -80,10 +84,6 @@ class MESSAGE_CENTER_EXPORT SlideOutController
// Decides which position the slide should go back after touch is released.
void CaptureControlOpenState();
// Slides the view out and closes it after the animation. The sign of
// |direction| indicates which way the slide occurs.
void SlideOutAndClose(int direction);
// Sets the opacity of the slide out layer if |update_opacity_| is true.
void SetOpacityIfNecessary(float opacity);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment