Jump to content

User:MSchottlender-WMF/Notifications UI Architecture

From mediawiki.org

This is an overview of the new Echo notifications UI architecture. The scope of this page is strictly the JavaScript front-end UI.

General structure

[edit]

The new Echo UI architecture was designed with the MVC model in mind. In general, it has three main components:

  • Controller (mw.echo.Controller) - The main action-setter of the code. It is responsible for setting up and calling actions through to the API and for building and manipulating the models.
  • Models (mw.echo.dm.*) - These are data components. They are roughly divided into two types - simple and complex.
    • Simple models - The simple models only contain pieces of data without much manipulation. They have setters and getters, and exist to serve as a convenient end-point to request consistent information throughout the different layers.
    • Complex models - The complex models contain pieces of data, but they can also manipulate it, format it, and emit events based on requested actions.
  • View/UI (mw.echo.ui.*) - These are the view elements that draw widgets on the screen. They listen to events from the model and populate themselves accordingly. Their actions are called through the controller.

MVC behavior

[edit]
The components are connected to one another through actions and events. The controller is the one initiating events to the API and building the model. The model emits events and contains data to be read by the UI.

As pointed out, the codebase now behaves under the MVC architecture. The order of action should remain strictly in one direction:

  • The widget is calling the controller for an action.
  • The controller calls the API and manipulates the models.
  • The models emit events (primarily an 'update' event) that the widgets listen to.
    • The widgets keep a reference to the model so they can listen to events and read its current state. Widgets should not manipulate the model directly.

Events

[edit]

In general, the system defines 'update' as its main event. All models emit this event to demonstrate that they have changed, and the prospective widgets listen to this event to update themselves.

The update event

[edit]

The 'update' event can be emitted from different layers in the system, to update a different scope.

Examples:

  • An update event from the ModelManager will cause the full NotificationsListWidget (the whole popup list) to be repopulated.
  • An update event from the NotificationsList data model will cause the specific bundled/group widget to update itself. In the case of cross-wiki notifications, each of the sub-groups listen to its own NotificationsList according to its source. When the Cross-wiki item is expanded, the actual notification items are populated into the model, which then emits an 'update' event, and causes the sub-group list widget to populate itself.

The scope of the update event can also be demonstrated in the filter and pagination models. In the Special:Notifications page, the mw.echo.ui.PaginationWidget listens to the mw.echo.dm.PaginationModel, which emits an 'update' event when its information is updated. The pagination widget then uses this event to update its state and decide whether to display certain buttons ('forward' / 'backwards' and 'home').

The same idea is true for the FiltersModel and the mw.echo.ui.ReadStateButtonSelectWidget which allows the user to choose a display of 'read', 'unread' and 'all' notifications in the Special:Notifications page.

Other events

[edit]

For the most part, the code is trying to consistently use the 'update' event. However, there are several cases where that is either not practical or not straightforward. In those cases, we use other events that the widgets can listen to.

Examples include:

  • discard event, emitted by mw.echo.dm.NotificationsList - In some cases (like in cross-wiki notifications, and in future local bundles) we want to permanently remove an item in case it is read. This is done through the 'discard' method, which emits a 'discard' event for the widget to deal with. In general, this is also done because there may be a difference in behavior when an item is discarded between cross-wiki notifications and local bundles.
  • removeSource event, emitted by mw.echo.dm.CrossWikiNotificationItem - If an entire cross-wiki source becomes empty (after all of its items are marked as read and discarded) then the source itself will be removed. The widget must recognize this to be able to respond accordingly.

Note that in the two cases above, we want a very specific, scoped response from the widget. If these behaviors would have been covered with an 'update' event, the entire widget would have re-populated itself, which in these cases is an overkill.

The model manager

[edit]

The model manager is responsible to be an entry-point and management for all the data models that the controller and widgets require. It serves as both a repository of required models as well as a single entity the widgets can listen to 'update' events on when the entire structure is rebuilt.

The model manager has these models:

  • Notifications models
  • FiltersModel
  • PaginationModel

Notifications model

[edit]

Notification models represent different types of notifications and their data. There can be, in principle, two types of notification models:

  • NotificationsList - contains an array of notification items
  • NotificationGroupList - contains an array of notification lists

General local notifications are stored in a local notifications list.

Cross-wiki notifications are all stored in a single data object (mw.echo.dm.CrossWikiNotificationItem) that contains a NotificationsGroupList. Each group represents a remote source.

The Special:Notifications page has a NotificationsInboxWidget that works similarly to the cross-wiki notifications widget, except in its NotificationsGroupList each group represents a single day of notifications.

Initially, cross-wiki notifications are populated with only their groups, leaving the notification items empty. When the widget itself is expanded, another request is sent to the server to also fill in the actual items in each group.

Naming

[edit]

Each notification model has a symbolic name. The names allow us to select and work with a specific model if we need to. We fetch those specific models through the ModelManager's getModel( name ) method.

  • 'local' - In the case of the popup, we will always have a 'local' model, even if it is empty, since the main notification list widget listens and expects it to be built. If it is empty, the widget will display a placeholder message.
  • 'xwiki' - If there is a cross-wiki bundle, its model will exist under the 'xwiki' symbolic name
  • bundles have their own naming scheme, as long as the name is unique.

Pagination model

[edit]

The pagination model is an extremely simple model that retains information about pagination (obviously). Every API request returns with a "continue" value, which the pagination model then stores according to the logical page it knows about. The widget can then update itself based on whether a "next" or "previous" page exists, and the controller can request the relevant "continue" value for the API request.

Filters model

[edit]

The filters model retains information about relevant filters for the Special:Notifications page, like readState ('read', 'unread' and 'all') and source and page information, in case the user is looking at remote notifications.

How the code works

[edit]
The relationship between models and UI elements in the popups

In general, an entire scaffolding is built for each of the popups (alerts and messages) and for the special page. The special page has slightly different behavior for the models, but uses the same base structure.

This section will outline how each of the popups are initialized and how they work:

  1. Basic Initialization
  2. Asynchronous building of the scaffolding
    1. Sending an initial API request
    2. Requesting and loading all ResourceLoader modules
    3. Building the models
  3. Popup opening
  4. Populating the notifications list

Initialization

[edit]

Since notifications load for all registered users, the initialization is done in two parts. See ext.echo.init.js for the initialization.

  • A minimal JavaScript script is initialized to listen to click events on either of the buttons.
  • When one of the buttons is clicked, the full code - including all required modules and libraries - are loaded.

When the button is clicked, the following is loaded:

  • An instance of mw.echo.api.EchoApi() is initialized and a request is made to fetch relevant notifications
  • ResourceLoader loads ext.echo.ui (desktop or mobile) and creates:
    • mw.echo.dm.UnreadNotificationsCounter
    • mw.echo.dm.ModelManager
    • mw.echo.Controller
    • and an mw.echo.ui.NotificationBadgeWidget it then uses to replace the current badge in the DOM.

The controller takes the API request that has already started and uses it to build the models and send them to the Model Manager.

The badge widget creates a NotificationsListWidget, which listens to the ModelManager for an update event. Once that is fired, it populates itself.

Building the models (Controller and ModelManager)

[edit]

When the controller receives the response from the API, it starts building the models and populates the ModelManager.

The models are built based on the information from the API. As mentioned above, each model represents a "group" of notifications. In the popup, that means that we have a group for the local notifications, a group (of groups) for the cross-wiki notification item, and groups for each local bundle (when we have them.)

Since the behavior of the popup is different than that of the Special:Notifications page, the models are also constructed differently.

The popup models

[edit]

The controller receives the information from the API and goes over the notification list. The list is comprised of notification data and a "fake" notification (with ID -1 and -2) for cross-wiki bundles.

  • For the local notifications, the controller creates an mw.echo.dm.NotificationItem and adds it to a mw.echo.dm.NotificationsList 'local' model.
  • For the cross-wiki item, we initially only have information about the external wikis that contain unread notifications, and the number in each. There are no items information yet. Since that's the case, the controller creates an mw.echo.dm.CrossWikiNotificationItem model 'xwiki' and then goes over the information about the sources we have. For each source, the controller:
    • Adds a remote API handler that utilizes mw.ForeignApi() so we can send remote requests for "mark as read" actions
    • Creates a mw.echo.dm.NotificationsList model for the specific source. It is still empty of items, but it is ready to be filled in when the cross-wiki widget is expanded.
  • For future cases of local bundles, they can be added to their own mw.echo.dm.NotificationList with their items as mw.echo.dm.NotificationItems.

The Special:Notifications models

[edit]

In the case of the special page, things are slightly different.

First, we don't show cross-wiki bundles (or any bundles for that matter). All notifications are unbundled in the special page, and access to cross-wiki notifications is done directly (through an upcoming side-bar filter.) That saves us the trouble of separating groups of notifications.

Second, the special page displays the list of notifications based on dates. It also allows operations on the entire date group (like 'mark as read' on an entire day.) In that aspect, it behaves similarly to the inner part (the expanded part) of a cross-wiki notification widget, and that is what the controller uses to create that data structure.

The controller accepts the information from the API and goes over the notifications. Since it doesn't have any bundles to consider, it just needs to divide notifications into days:

  • For every notification, it creates an mw.echo.dm.NotificationItem
  • That item is collected into an object whose keys are dates. We end up with an object of dates and their relevant items.
  • Each of those dates becomes an mw.echo.dm.NotificationsList with a unique symbolic name to identify it in the Model Manager. We do not have 'local' or 'xwiki' models in the Special:Notifications page.

Populating the data (Widgets)

[edit]