Commit 5c5c6176 authored by Mathias Carlen's avatar Mathias Carlen Committed by Commit Bot

[Autofill Assistant] Auto expand/collapse for prompt.

Before this patch when entering the prompt state, the sheet would always
be expanded so that the buttons were visible to a user. This patch
introduces a new proto field that allows this behavior to be configured
from the backend. Additionally, the configure bottom sheet action is
extended by a new field to expand or collapse the sheet
programmatically.

Bug: b/145204744
Change-Id: I9191e3e4f7b6b688d6a9dae800f730b56056b243
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2004868
Commit-Queue: Mathias Carlen <mcarlen@chromium.org>
Reviewed-by: default avatarClemens Arbesser <arbesser@google.com>
Cr-Commit-Position: refs/heads/master@{#734411}
parent 1c8e0177
......@@ -194,7 +194,7 @@ class AssistantBottomBarCoordinator
model.addObserver((source, propertyKey) -> {
if (AssistantModel.VISIBLE == propertyKey) {
if (model.get(AssistantModel.VISIBLE)) {
showAndExpand();
showContentAndExpand();
} else {
hide();
}
......@@ -277,7 +277,7 @@ class AssistantBottomBarCoordinator
}
/** Request showing the Assistant bottom bar view and expand the sheet. */
public void showAndExpand() {
public void showContentAndExpand() {
BottomSheetUtils.showContentAndExpand(
mBottomSheetController, mContent, /* animate= */ true);
}
......@@ -301,6 +301,16 @@ class AssistantBottomBarCoordinator
maybeShowHeaderChip();
}
/** Expand the bottom sheet. */
void expand() {
mBottomSheetController.expandSheet();
}
/** Collapse the bottom sheet to the peek mode. */
void collapse() {
mBottomSheetController.collapseSheet(/* animate = */ true);
}
@Override
public void setShowOnlyCarousels(boolean showOnlyCarousels) {
mScrollableContent.setVisibility(showOnlyCarousels ? View.GONE : View.VISIBLE);
......
......@@ -192,9 +192,19 @@ class AutofillAssistantUiController {
}
}
@CalledByNative
private void showContentAndExpandBottomSheet() {
mCoordinator.getBottomBarCoordinator().showContentAndExpand();
}
@CalledByNative
private void expandBottomSheet() {
mCoordinator.getBottomBarCoordinator().showAndExpand();
mCoordinator.getBottomBarCoordinator().expand();
}
@CalledByNative
private void collapseBottomSheet() {
mCoordinator.getBottomBarCoordinator().collapse();
}
@CalledByNative
......
......@@ -326,6 +326,9 @@ void UiControllerAndroid::Attach(content::WebContents* web_contents,
OnViewportModeChanged(ui_delegate->GetViewportMode());
OnPeekModeChanged(ui_delegate->GetPeekMode());
OnFormChanged(ui_delegate->GetForm());
// TODO(b/145204744): Store the collapsed or expanded state from the bottom
// sheet when detaching the UI so that it can be restored appropriately
// here.
UiDelegate::OverlayColors colors;
ui_delegate->GetOverlayColors(&colors);
......@@ -369,6 +372,8 @@ void UiControllerAndroid::OnStateChanged(AutofillAssistantState new_state) {
void UiControllerAndroid::SetupForState() {
UpdateActions(ui_delegate_->GetUserActions());
AutofillAssistantState state = ui_delegate_->GetState();
bool should_prompt_action_expand_sheet =
ui_delegate_->ShouldPromptActionExpandSheet();
switch (state) {
case AutofillAssistantState::STARTING:
SetOverlayState(OverlayState::FULL);
......@@ -387,8 +392,8 @@ void UiControllerAndroid::SetupForState() {
AllowShowingSoftKeyboard(true);
SetSpinPoodle(false);
// user interaction is needed.
ExpandBottomSheet();
if (should_prompt_action_expand_sheet)
ShowContentAndExpandBottomSheet();
return;
case AutofillAssistantState::PROMPT:
......@@ -396,8 +401,8 @@ void UiControllerAndroid::SetupForState() {
AllowShowingSoftKeyboard(true);
SetSpinPoodle(false);
// user interaction is needed.
ExpandBottomSheet();
if (should_prompt_action_expand_sheet)
ShowContentAndExpandBottomSheet();
return;
case AutofillAssistantState::MODAL_DIALOG:
......@@ -412,7 +417,7 @@ void UiControllerAndroid::SetupForState() {
SetSpinPoodle(false);
// Make sure the user sees the error message.
ExpandBottomSheet();
ShowContentAndExpandBottomSheet();
Detach();
return;
......@@ -469,6 +474,16 @@ void UiControllerAndroid::OnPeekModeChanged(
java_object_, peek_mode);
}
void UiControllerAndroid::OnExpandBottomSheet() {
Java_AutofillAssistantUiController_expandBottomSheet(AttachCurrentThread(),
java_object_);
}
void UiControllerAndroid::OnCollapseBottomSheet() {
Java_AutofillAssistantUiController_collapseBottomSheet(AttachCurrentThread(),
java_object_);
}
void UiControllerAndroid::OnOverlayColorsChanged(
const UiDelegate::OverlayColors& colors) {
JNIEnv* env = AttachCurrentThread();
......@@ -486,9 +501,9 @@ void UiControllerAndroid::AllowShowingSoftKeyboard(bool enabled) {
enabled);
}
void UiControllerAndroid::ExpandBottomSheet() {
Java_AutofillAssistantUiController_expandBottomSheet(AttachCurrentThread(),
java_object_);
void UiControllerAndroid::ShowContentAndExpandBottomSheet() {
Java_AutofillAssistantUiController_showContentAndExpandBottomSheet(
AttachCurrentThread(), java_object_);
}
void UiControllerAndroid::SetSpinPoodle(bool enabled) {
......
......@@ -103,6 +103,8 @@ class UiControllerAndroid : public ControllerObserver {
void OnViewportModeChanged(ViewportMode mode) override;
void OnPeekModeChanged(
ConfigureBottomSheetProto::PeekMode peek_mode) override;
void OnExpandBottomSheet() override;
void OnCollapseBottomSheet() override;
void OnOverlayColorsChanged(const UiDelegate::OverlayColors& colors) override;
void OnFormChanged(const FormProto* form) override;
void OnClientSettingsChanged(const ClientSettings& settings) override;
......@@ -203,7 +205,7 @@ class UiControllerAndroid : public ControllerObserver {
void SetOverlayState(OverlayState state);
void AllowShowingSoftKeyboard(bool enabled);
void ExpandBottomSheet();
void ShowContentAndExpandBottomSheet();
void SetSpinPoodle(bool enabled);
std::string GetDebugContext();
void DestroySelf();
......
......@@ -107,8 +107,8 @@ class ActionDelegate {
// scripts, even though we're in the middle of a script. This includes
// allowing access to the touchable elements set previously, in the same
// script.
virtual void Prompt(
std::unique_ptr<std::vector<UserAction>> user_actions) = 0;
virtual void Prompt(std::unique_ptr<std::vector<UserAction>> user_actions,
bool disable_force_expand_sheet) = 0;
// Have the UI leave the prompt state and go back to its previous state.
virtual void CleanUpAfterPrompt() = 0;
......@@ -314,6 +314,13 @@ class ActionDelegate {
// Checks the current peek mode.
virtual ConfigureBottomSheetProto::PeekMode GetPeekMode() = 0;
// Expands the bottom sheet. This is the same as the user swiping up.
virtual void ExpandBottomSheet() = 0;
// Collapses the bottom sheet to the current peek state as set by
// |SetPeekMode|. This is the same as the user swiping down.
virtual void CollapseBottomSheet() = 0;
// Calls the callback once the main document window has been resized.
virtual void WaitForWindowHeightChange(
base::OnceCallback<void(const ClientStatus&)> callback) = 0;
......
......@@ -435,7 +435,8 @@ void CollectUserDataAction::OnShowToUser(UserData* user_data,
if (collect_user_data.has_prompt()) {
delegate_->SetStatusMessage(collect_user_data.prompt());
}
delegate_->Prompt(nullptr);
delegate_->Prompt(/* user_actions = */ nullptr,
/* disable_force_expand_sheet = */ false);
delegate_->CollectUserData(collect_user_data_options_.get());
}
......
......@@ -71,6 +71,11 @@ void ConfigureBottomSheetAction::InternalProcessAction(
delegate_->SetPeekMode(proto.peek_mode());
}
if (proto.has_expand() && proto.expand())
delegate_->ExpandBottomSheet();
if (proto.has_collapse() && proto.collapse())
delegate_->CollapseBottomSheet();
if (callback) {
UpdateProcessedAction(OkClientStatus());
std::move(callback).Run(std::move(processed_action_proto_));
......
......@@ -67,8 +67,9 @@ class MockActionDelegate : public ActionDelegate {
ClickAction::ClickType click_type,
base::OnceCallback<void(const ClientStatus&)> callback));
MOCK_METHOD1(Prompt,
void(std::unique_ptr<std::vector<UserAction>> user_actions));
MOCK_METHOD2(Prompt,
void(std::unique_ptr<std::vector<UserAction>> user_actions,
bool disable_force_expand_sheet));
MOCK_METHOD0(CleanUpAfterPrompt, void());
void FillAddressForm(
......@@ -225,6 +226,8 @@ class MockActionDelegate : public ActionDelegate {
MOCK_METHOD1(SetPeekMode,
void(ConfigureBottomSheetProto::PeekMode peek_mode));
MOCK_METHOD0(GetPeekMode, ConfigureBottomSheetProto::PeekMode());
MOCK_METHOD0(ExpandBottomSheet, void());
MOCK_METHOD0(CollapseBottomSheet, void());
MOCK_METHOD3(
SetForm,
bool(std::unique_ptr<FormProto> form,
......@@ -269,6 +272,7 @@ class MockActionDelegate : public ActionDelegate {
}
MOCK_METHOD0(RequireUI, void());
MOCK_METHOD0(SetExpandSheetForPromptAction, bool());
const ClientSettings& GetSettings() override { return client_settings_; }
......
......@@ -147,7 +147,8 @@ void PromptAction::UpdateUserActions() {
weak_ptr_factory_.GetWeakPtr(), i));
user_actions->emplace_back(std::move(user_action));
}
delegate_->Prompt(std::move(user_actions));
delegate_->Prompt(std::move(user_actions),
proto_.prompt().disable_force_expand_sheet());
precondition_changed_ = false;
}
......
......@@ -45,9 +45,10 @@ class PromptActionTest : public testing::Test {
EXPECT_CALL(mock_action_delegate_, OnWaitForDom(_, _, _, _))
.WillRepeatedly(Invoke(this, &PromptActionTest::FakeWaitForDom));
ON_CALL(mock_action_delegate_, Prompt(_))
.WillByDefault(Invoke(
[this](std::unique_ptr<std::vector<UserAction>> user_actions) {
ON_CALL(mock_action_delegate_, Prompt(_, _))
.WillByDefault(
Invoke([this](std::unique_ptr<std::vector<UserAction>> user_actions,
bool disable_force_expand_sheet) {
user_actions_ = std::move(user_actions);
}));
prompt_proto_ = proto_.mutable_prompt();
......
......@@ -58,7 +58,8 @@ void ShowFormAction::OnFormValuesChanged(const FormProto::Result* form_result) {
auto user_actions = std::make_unique<std::vector<UserAction>>();
user_actions->emplace_back(std::move(user_action));
delegate_->Prompt(std::move(user_actions));
delegate_->Prompt(std::move(user_actions),
/* disable_force_expand_sheet = */ false);
}
void ShowFormAction::OnCancelForm(const ClientStatus& status) {
......
......@@ -314,6 +314,10 @@ void Controller::RemoveListener(ScriptExecutorDelegate::Listener* listener) {
listeners_.erase(found);
}
void Controller::SetExpandSheetForPromptAction(bool expand) {
expand_sheet_for_prompt_action_ = expand;
}
bool Controller::PerformUserActionWithContext(
int index,
std::unique_ptr<TriggerContext> context) {
......@@ -354,6 +358,24 @@ void Controller::SetPeekMode(ConfigureBottomSheetProto::PeekMode peek_mode) {
}
}
void Controller::ExpandBottomSheet() {
for (ControllerObserver& observer : observers_) {
// TODO(crbug/806868): The interface here and in some of the other On*
// events should be coming from the UI layer, not the controller. Or at
// least be renamed to something like On*Requested.
observer.OnExpandBottomSheet();
}
}
void Controller::CollapseBottomSheet() {
for (ControllerObserver& observer : observers_) {
// TODO(crbug/806868): The interface here and in some of the other On*
// events should be coming from the UI layer, not the controller. Or at
// least be renamed to something like On*Requested.
observer.OnCollapseBottomSheet();
}
}
const FormProto* Controller::GetForm() const {
return form_.get();
}
......@@ -477,6 +499,10 @@ EventHandler* Controller::GetEventHandler() {
return &event_handler_;
}
bool Controller::ShouldPromptActionExpandSheet() const {
return expand_sheet_for_prompt_action_;
}
void Controller::AddObserver(ControllerObserver* observer) {
observers_.AddObserver(observer);
}
......
......@@ -123,6 +123,8 @@ class Controller : public ScriptExecutorDelegate,
std::unique_ptr<std::vector<UserAction>> user_actions) override;
void SetViewportMode(ViewportMode mode) override;
void SetPeekMode(ConfigureBottomSheetProto::PeekMode peek_mode) override;
void ExpandBottomSheet() override;
void CollapseBottomSheet() override;
bool SetForm(
std::unique_ptr<FormProto> form,
base::RepeatingCallback<void(const FormProto::Result*)> changed_callback,
......@@ -137,6 +139,8 @@ class Controller : public ScriptExecutorDelegate,
void AddListener(ScriptExecutorDelegate::Listener* listener) override;
void RemoveListener(ScriptExecutorDelegate::Listener* listener) override;
void SetExpandSheetForPromptAction(bool expand) override;
bool EnterState(AutofillAssistantState state) override;
void SetCollectUserDataOptions(CollectUserDataOptions* options) override;
void WriteUserData(
......@@ -207,6 +211,7 @@ class Controller : public ScriptExecutorDelegate,
const ValueProto& value) override;
UserModel* GetUserModel() override;
EventHandler* GetEventHandler() override;
bool ShouldPromptActionExpandSheet() const override;
private:
friend ControllerTest;
......@@ -375,6 +380,7 @@ class Controller : public ScriptExecutorDelegate,
// Current peek mode.
ConfigureBottomSheetProto::PeekMode peek_mode_ =
ConfigureBottomSheetProto::HANDLE;
bool auto_change_peek_mode_ = false;
std::unique_ptr<OverlayColors> overlay_colors_;
......@@ -438,6 +444,8 @@ class Controller : public ScriptExecutorDelegate,
EventHandler event_handler_;
UserModel user_model_;
bool expand_sheet_for_prompt_action_ = true;
base::WeakPtrFactory<Controller> weak_ptr_factory_{this};
DISALLOW_COPY_AND_ASSIGN(Controller);
......
......@@ -98,6 +98,12 @@ class ControllerObserver : public base::CheckedObserver {
virtual void OnPeekModeChanged(
ConfigureBottomSheetProto::PeekMode peek_mode) = 0;
// Called when the bottom sheet should be expanded.
virtual void OnExpandBottomSheet() = 0;
// Called when the bottom sheet should be collapsed.
virtual void OnCollapseBottomSheet() = 0;
// Called when the overlay colors have changed.
virtual void OnOverlayColorsChanged(
const UiDelegate::OverlayColors& colors) = 0;
......
......@@ -143,6 +143,16 @@ ConfigureBottomSheetProto::PeekMode FakeScriptExecutorDelegate::GetPeekMode() {
return peek_mode_;
}
void FakeScriptExecutorDelegate::ExpandBottomSheet() {
expand_or_collapse_updated_ = true;
expand_or_collapse_value_ = true;
}
void FakeScriptExecutorDelegate::CollapseBottomSheet() {
expand_or_collapse_updated_ = true;
expand_or_collapse_value_ = false;
}
bool FakeScriptExecutorDelegate::HasNavigationError() {
return navigation_error_;
}
......@@ -163,6 +173,10 @@ void FakeScriptExecutorDelegate::RemoveListener(Listener* listener) {
listeners_.erase(listener);
}
void FakeScriptExecutorDelegate::SetExpandSheetForPromptAction(bool expand) {
expand_sheet_for_prompt_ = expand;
}
bool FakeScriptExecutorDelegate::SetForm(
std::unique_ptr<FormProto> form,
base::RepeatingCallback<void(const FormProto::Result*)> changed_callback,
......
......@@ -57,6 +57,8 @@ class FakeScriptExecutorDelegate : public ScriptExecutorDelegate {
ViewportMode GetViewportMode() override;
void SetPeekMode(ConfigureBottomSheetProto::PeekMode peek_mode) override;
ConfigureBottomSheetProto::PeekMode GetPeekMode() override;
void ExpandBottomSheet() override;
void CollapseBottomSheet() override;
bool SetForm(
std::unique_ptr<FormProto> form,
base::RepeatingCallback<void(const FormProto::Result*)> changed_callback,
......@@ -68,6 +70,7 @@ class FakeScriptExecutorDelegate : public ScriptExecutorDelegate {
void RequireUI() override;
void AddListener(Listener* listener) override;
void RemoveListener(Listener* listener) override;
void SetExpandSheetForPromptAction(bool expand) override;
ClientSettings* GetMutableSettings() { return &client_settings_; }
......@@ -132,6 +135,10 @@ class FakeScriptExecutorDelegate : public ScriptExecutorDelegate {
ViewportMode viewport_mode_ = ViewportMode::NO_RESIZE;
ConfigureBottomSheetProto::PeekMode peek_mode_ =
ConfigureBottomSheetProto::HANDLE;
bool expand_or_collapse_updated_ = false;
bool expand_or_collapse_value_ = false;
bool expand_sheet_for_prompt_ = true;
bool require_ui_ = false;
DISALLOW_COPY_AND_ASSIGN(FakeScriptExecutorDelegate);
......
......@@ -47,6 +47,8 @@ class MockControllerObserver : public ControllerObserver {
MOCK_METHOD1(OnViewportModeChanged, void(ViewportMode mode));
MOCK_METHOD1(OnPeekModeChanged,
void(ConfigureBottomSheetProto::PeekMode peek_mode));
MOCK_METHOD0(OnExpandBottomSheet, void());
MOCK_METHOD0(OnCollapseBottomSheet, void());
MOCK_METHOD1(OnOverlayColorsChanged,
void(const UiDelegate::OverlayColors& colors));
MOCK_METHOD1(OnFormChanged, void(const FormProto* form));
......
......@@ -272,8 +272,11 @@ void ScriptExecutor::OnGetFullCard(GetFullCardCallback callback,
}
void ScriptExecutor::Prompt(
std::unique_ptr<std::vector<UserAction>> user_actions) {
std::unique_ptr<std::vector<UserAction>> user_actions,
bool disable_force_expand_sheet) {
// First communicate to the delegate that prompt actions should or should not
// expand the sheet intitially.
delegate_->SetExpandSheetForPromptAction(!disable_force_expand_sheet);
if (delegate_->EnterState(AutofillAssistantState::PROMPT) &&
touchable_element_area_) {
// Prompt() reproduces the end-of-script appearance and behavior during
......@@ -305,6 +308,7 @@ void ScriptExecutor::CleanUpAfterPrompt() {
touchable_element_area_.reset();
delegate_->ClearTouchableElementArea();
delegate_->SetExpandSheetForPromptAction(true);
delegate_->EnterState(AutofillAssistantState::RUNNING);
}
......@@ -545,6 +549,14 @@ ConfigureBottomSheetProto::PeekMode ScriptExecutor::GetPeekMode() {
return delegate_->GetPeekMode();
}
void ScriptExecutor::ExpandBottomSheet() {
return delegate_->ExpandBottomSheet();
}
void ScriptExecutor::CollapseBottomSheet() {
return delegate_->CollapseBottomSheet();
}
void ScriptExecutor::WaitForWindowHeightChange(
base::OnceCallback<void(const ClientStatus&)> callback) {
delegate_->GetWebController()->WaitForWindowHeightChange(std::move(callback));
......
......@@ -126,7 +126,8 @@ class ScriptExecutor : public ActionDelegate,
void WriteUserData(
base::OnceCallback<void(UserData*, UserData::FieldChange*)>) override;
void GetFullCard(GetFullCardCallback callback) override;
void Prompt(std::unique_ptr<std::vector<UserAction>> user_actions) override;
void Prompt(std::unique_ptr<std::vector<UserAction>> user_actions,
bool disable_force_expand_sheet) override;
void CleanUpAfterPrompt() override;
void FillAddressForm(
const autofill::AutofillProfile* profile,
......@@ -210,6 +211,8 @@ class ScriptExecutor : public ActionDelegate,
ViewportMode GetViewportMode() override;
void SetPeekMode(ConfigureBottomSheetProto::PeekMode peek_mode) override;
ConfigureBottomSheetProto::PeekMode GetPeekMode() override;
void ExpandBottomSheet() override;
void CollapseBottomSheet() override;
void WaitForWindowHeightChange(
base::OnceCallback<void(const ClientStatus&)> callback) override;
const ClientSettings& GetSettings() override;
......
......@@ -86,6 +86,8 @@ class ScriptExecutorDelegate {
virtual void SetViewportMode(ViewportMode mode) = 0;
virtual void SetPeekMode(ConfigureBottomSheetProto::PeekMode peek_mode) = 0;
virtual ConfigureBottomSheetProto::PeekMode GetPeekMode() = 0;
virtual void ExpandBottomSheet() = 0;
virtual void CollapseBottomSheet() = 0;
virtual bool SetForm(
std::unique_ptr<FormProto> form,
base::RepeatingCallback<void(const FormProto::Result*)> changed_callback,
......@@ -134,6 +136,9 @@ class ScriptExecutorDelegate {
// exists.
virtual void RemoveListener(Listener* listener) = 0;
// Set how the sheet should behave when entering a prompt state.
virtual void SetExpandSheetForPromptAction(bool expand) = 0;
protected:
virtual ~ScriptExecutorDelegate() {}
};
......
......@@ -1229,6 +1229,10 @@ message PromptProto {
// If true, run scripts flagged with 'interrupt=true' as soon as their
// preconditions match, then go back to the prompt.
optional bool allow_interrupt = 5;
// If this is true, then we do not force expand the sheet when entering the
// prompt state. When false or not set, this keeps the default behavior.
optional bool disable_force_expand_sheet = 6;
}
message ContactDetailsProto {
......@@ -1673,6 +1677,10 @@ message ConfigureBottomSheetProto {
// Show swipe handle, header, progress bar, suggestions and actions.
HANDLE_HEADER_CAROUSELS = 3;
// Entirely hide the bottom sheet when collapsed.
// TBD:
HIDDEN = 4;
}
// Whether the viewport should be resized. Resizing the viewport is an
......@@ -1680,15 +1688,25 @@ message ConfigureBottomSheetProto {
// cautiously.
optional ViewportResizing viewport_resizing = 1;
// Set the peek mode. This will change the peek height of the sheet. If
// viewport_resizing is set to RESIZE_LAYOUT_VIEWPORT or was set by a previous
// ConfigureBottomSheet action, the viewport will be resized to match the new
// peek height.
// Set the peek mode. This will change the peek mode of the sheet without
// actually updating the sheet to that setting. If viewport_resizing is set
// to RESIZE_LAYOUT_VIEWPORT or was set by a previous ConfigureBottomSheet
// action, the viewport will be resized to match the new peek height.
optional PeekMode peek_mode = 2;
// Maximum time to wait for the window to resize before continuing with the
// script. If 0 or unset, the action doesn't wait.
optional int32 resize_timeout_ms = 3;
// Option to automatically expand or collapse the sheet or leave as is.
oneof apply_state {
// Go to the expanded state (same as if the user swiped the bottom sheet
// up).
bool expand = 4;
// Go to the peek state (same as if the user swiped the bottom
// sheet down).
bool collapse = 5;
}
}
// Allow scripts to display a form with multiple inputs.
......
......@@ -174,6 +174,7 @@ class UiDelegate {
// Returns whether the viewport should be resized.
virtual ViewportMode GetViewportMode() = 0;
// Peek mode state and whether it was changed automatically last time.
virtual ConfigureBottomSheetProto::PeekMode GetPeekMode() = 0;
// Fills in the overlay colors.
......@@ -212,6 +213,9 @@ class UiDelegate {
// Returns the event handler.
virtual EventHandler* GetEventHandler() = 0;
// Whether the sheet should be auto expanded when entering the prompt state.
virtual bool ShouldPromptActionExpandSheet() const = 0;
protected:
protected:
UiDelegate() = default;
......
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