diff --git a/QuestionScreen.md b/QuestionScreen.md index f1427fb..6c7ab4e 100644 --- a/QuestionScreen.md +++ b/QuestionScreen.md @@ -1,6 +1,45 @@ -# QuestionScreen +# Introducing the screens concept + +A screen is an special type of question, extending the Botman question, designed to wrap a field question and control the interaction of the user with the question. + + + +# Explaining the three levels of interaction + +Here's how the different levels look: + +[https://whimsical.com/7VzPWjnvBo7q8XjJ2tgZ9L](https://whimsical.com/7VzPWjnvBo7q8XjJ2tgZ9L) + +And here is how each level work: + +## **Survey Conversation level:** + +Has the survey and fields data. Creates question objects and pass them to the screens. It communicates with screens only. + +- It gets the text content from the screen and passes it to botman. +- When the user sends something, it takes the answer from botman and passes it to the screen. +- If the screen indicates that it should be sent back again to the user, the question screen is repeated with it's new content (maybe is the next/previous page, or hints, or errors, we'll never know at this level). This step will repeat until the question screen indicates otherwise, meaning that there will not be more interaction with the user for that question. +- After that, the flow will continue as it is already designed. + +## **Question Screen level:** + +Everything the user see, read, send or interact with have to go through this level. Things like getting the payload from the field question can be done by calling the methods on the the field question itself. + +- It gets the field question label, instructions, options, hints, errors, etc from the field question and paginates accordingly by taking into consideration things like the max characters limit stablished and the amount of characters reserved to include navigation options. +- It intercepts all the answers passed from the survey conversation to verify if the user is trying to navigate or interact with the question before answering. If the input sent is not recognized as a known trigger and there is not any other condition to prevent it, the answer will be passed to the field question for evaluation. +- It uses the the field question to determine which options to include, for example: Skip question, Show more info, etc + +## **Field Question Level:** + +This is where translations, validations, and everything else happens. In other words, anything field-specific or related to the field data happens at this level. + + +# The QuestionScreen class In order to separate concerns between asnwering or choosing a option and interacting and navigating through the content of a question, we created the Question Screen. Which basically is a Botman Question wrapping our FieldQuestions. This allow us to pass the question screens to the botman bot, as questions, and handle all user interactions for the question in context. The question screen takes all the text pieces from the FieldQuestion and paginate them, adding navigation options also. + +**Important**: +This does not apply for transitions between questions or steps. diff --git a/app/Messages/Outgoing/Screen/AbstractScreen.php b/app/Messages/Outgoing/Screen/AbstractScreen.php index efac212..8d752ff 100644 --- a/app/Messages/Outgoing/Screen/AbstractScreen.php +++ b/app/Messages/Outgoing/Screen/AbstractScreen.php @@ -20,8 +20,18 @@ abstract class AbstractScreen extends Question */ protected $done = false; + /** + * Indicates if the screen should have the cancel option. + * + * @var bool + */ protected $includeCancelOption; + /** + * Indicates if the screen was canceled by the user. + * + * @var bool + */ protected $canceled = false; public function __construct(bool $includeCancelOption = true) @@ -31,15 +41,45 @@ public function __construct(bool $includeCancelOption = true) parent::__construct($this->getText()); } + /** + * {@inheritdoc} + * + * Returns current page's text as the text Botman should show + * when asking the question + */ public function getText(): string { return $this->currentPage->getText(); } + /** + * Generates the initial page for the screen. + * If the text content does not fit in a page, considering the limited + * amount of characters, the page will automatically create and save the + * reference to the next page, this happens recursively. + * + * @return Page + */ abstract public function buildInitialPage(): Page; + /** + * Implement how the screen will handle an answer from Botman. + * + * It could pass the answer to another question or ignore it, + * it will depend on the actual implementation. + * + * @param Answer $answer + * @return void + */ abstract public function setAnswer(Answer $answer): void; + /** + * Indicates if within a conversation context this screen is done, + * meaning that if it is, the conversation can move forward on the flow. + * If it is not, the screen is usually repeated. + * + * @return bool + */ public function isDone(): bool { return $this->done; @@ -50,17 +90,33 @@ public function wasCanceled(): bool return $this->canceled; } + /** + * Sets the screen as canceled and indicates to not reapat this screen again. + * + * @return void + */ private function cancel() { $this->canceled = true; $this->dontRepeatAgain(); } + /** + * Marks the screen as done. + * + * @return void + */ public function dontRepeatAgain(): void { $this->done = true; } + /** + * Returns the array of options that should be available on all the pages + * that may come out of this screen. + * + * @return array + */ public function getDefaultScreenOptions(): array { $options = []; @@ -73,6 +129,12 @@ public function getDefaultScreenOptions(): array return $options; } + /** + * Map each screen option to it's correspondent action. + * + * @param string $option + * @return void + */ public function handleScreenOption(string $option): void { if (self::isEqualToOption($option, __('conversation.screen.next.value'))) { @@ -86,6 +148,11 @@ public function handleScreenOption(string $option): void } } + /** + * Sets the current page to the next one referenced in the current page. + * + * @return void + */ public function transitionToNextPage(): void { if ($this->currentPage->hasNext()) { @@ -93,6 +160,11 @@ public function transitionToNextPage(): void } } + /** + * Sets the current page to the previous page referenced in the current page. + * + * @return void + */ public function transitionToPreviousPage(): void { if ($this->currentPage->hasPrevious()) { @@ -100,6 +172,12 @@ public function transitionToPreviousPage(): void } } + /** + * Creates a new page for the error message provided and set it as current page. + * + * @param string $errorMessage + * @return void + */ public function transitionToErrorPage(string $errorMessage): void { $errorPage = new Page($errorMessage, [], [], $this->currentPage); @@ -117,6 +195,15 @@ public function getCurrentPage(): Page return $this->currentPage; } + /** + * Compares the option chosen by the user to a previously registered option. + * + * This allows options to be case insensitive. + * + * @param string $selectedOption + * @param string $option + * @return bool + */ public static function isEqualToOption(string $selectedOption, string $option): bool { return strcasecmp($selectedOption, $option) === 0; diff --git a/app/Messages/Outgoing/Screen/Page.php b/app/Messages/Outgoing/Screen/Page.php index 7b51cba..4a22aa9 100644 --- a/app/Messages/Outgoing/Screen/Page.php +++ b/app/Messages/Outgoing/Screen/Page.php @@ -14,6 +14,8 @@ class Page /** * Registered options for this page's scope. * + * Used to check is an option is supported for this page. + * * @var array */ protected $pageOptions = []; @@ -61,6 +63,9 @@ public function __construct(?string $text, array $screenOptions = [], array $opt $builder->appendText($text); $text = null; } else { + /** + * Append as much as possible and returns the rest to use it in the other page. + */ $text = $builder->appendExceedingText($text); } } @@ -78,6 +83,9 @@ public function __construct(?string $text, array $screenOptions = [], array $opt if ($builder->hasEnoughSpaceForText($optionText, $isTheLastPiecePendingForAttachment)) { $builder->appendText($optionText); } else { + /** + * Append as much as possible and returns the rest to use it in the other page. + */ $newOptionText = $builder->appendExceedingText($optionText); $option->text = $newOptionText; array_unshift($options, $option); @@ -90,7 +98,9 @@ public function __construct(?string $text, array $screenOptions = [], array $opt $this->next = new self($text, $screenOptions, $options, $this); } + // all the appended text, including options $this->text = $builder->getPageContent(); + // page options for this page $this->pageOptions = $builder->getPageOptionsValues(); }