Extension:Echo/Creating a new notification type

This page is a translated version of the page Extension:Echo/Creating a new notification type and the translation is 100% complete.

Notifikační systém umožňuje přihlášeným uživatelům přijímat upozornění a oznámení i z jiných instancí MediaWiki, pokud jsou do tohoto systému zapojeny. U všech projektů Wikimedie tak dostávají uživatelé oznámení téměř ze všech wikinách, provozovaných v rámci jejího clusteru. Ovšem tento notifikační systém mohou využívat i třetí strany, které do něj zapojeny nejsou. Tzv. "třetí strany" ho mohou používat v rámci jedné wiki, ale i celé skupiny vzájemně propojených wikin (a mít tak pro ně společný systém upozornění).

Video na kterém je demonstrováno, jak vytvořit nový typ upozornění, byl prezentován v roce 2017 v rámci Setkání vývojářů Wikimedie.

Tento systém umožňuje vývojářům rozšíření generovat nové typy oznámení, které pak mohou za splnění určitých podmínek dostávat ostatní uživatelé. Tento dokument popisuje jakým způsobem se takové oznámení tvoří, vysvětluje osvědčené postupy a upozorňuje na úskalí, kterým je při tom potřeba se vyhnout.

Jak notifikace fungují - úvod

Upozornění, velmi obecně řečeno, je založeno na dvou konceptech - události (Event) a prezentačním modelu (PresentationModel), který jej zpracuje do nějaké akce. Událost ukládá detaily spojené s událostí a prezentační model definuje, jakým způsobem se má tato událost prezentovat v prostředí uživatelského rozhraní.

EchoEvent

Třída EchoEvent definuje obecné události, shromažďuje informace o související stránce, uživateli a wiki a vkládá je do databáze. Události jsou obecné a reagovat lze na ně i několika různými způsoby, pokud jsou nadefinovány. Dejme tomu, že zmíníte 4 osoby, což vytvoří jednu událost, na kterou ale budou odkazovat (a přes 'trigger' svázaná) oznámení pro tyto čtyři zmíněné uživatele. Všechny tyto jejich individuální notifikace však budou odkazovat na jednu a tu samou událost.

EchoPresentationModel

Každý typ notifikace tak vyžaduje svůj prezentační model. Způsob, který bude definovat, co se v oznámení zobrazí, kam se bude odkazovat a jaké další sekundární akce s tím mají být spojeny. Front-endové komponenty notifikačního systému (vyskakovací okno s oznámením a stránka Special:Notifications) tyto informace načtou a na jejich základě vygenerují příslušné oznámení.

Typ notifikace pokaždé vychází z výchozí definice ve třídě EchoEventPresentationModel. Tato třída definuje základní chování všech oznámení a každé podřízené oznámení ji nejprve musí rozšířit a následně přizpůsobit detaily svým potřebám.

Jak fungují události

Pokud chcete vytvořit nový typ oznámení, měli byste nejprve vyvolat příslušnou událost. Logický postup je následující:

  1. Pokud je to na místě, vytvoří váš kód událost prostřednictvím EchoEvent::create()
    1. V rámci definice události byste měli podrobně popsat, kteří uživatelé mají obdržet oznámení o této události. To je definováno parametrem 'user-locators' => array( ... ). (Na příklad toho, jak je to uděláno se podívejte do metody 'user-locators' u rozšíření Flow, která slouží k vyhledávání uživatelů, kteří sledují článek s konkrétním názvem).
    2. Příslušná událost se uloží do databáze a příslušným uživatelům se následně začne zobrazovat odpovídající oznámení.
    3. Když si uživatelé vyžádají seznam oznámení z rozhraní API, systém vyhledá příslušný model prezentace a vytvoří data - název, primární odkaz, sekundární odkazy, příslušného uživatele atd.
    4. Ke zobrazení příslušného oznámení použije front-end následně uložená strukturovaná data.
Pro větší názornost jsme popis celého procesu mírně zjednodušili. Podrobné vysvětlení toho, jak to funguje v praxi, včetně popisu databázových tabulek (uživatelů, wiki, odkazů mimo wiki atd.), je přesahuje rámec tohoto tutoriálu.

Definice

Definice události

Definice událostí se vyskytují v metodě, která reaguje na háček BeforeCreateEchoEvent. Tento hák obsahuje tři proměnné: $notifications, $notificationCategories, $icons.

Definování kategorie události

Uživatelé mají obecně povoleno zaškrtnout/zrušit zaškrtnutí možnosti dostávat upozornění na web nebo e-mail v předvolbách. Kategorie předvoleb má nápovědu a měla by být definována jako $notificationCategories. Název kategorie se používá jako klíč a je tím, na co pak odkazuje definice události v parametru "category". Musíte také definovat zprávu s názvem echo-category-title-my-category a nahradit my-category skutečným názvem kategorie. Toto se používá v Special:Preferences na kartě Oznámení.

Definování ikony oznámení

Upozornění se zobrazují s ikonami. Pokud ikona není nastavena, zobrazí se výchozí ikona. Pokud je ikona nová, musí být načtena a definována pomocí $icons.

Definujte ikonu pomocí celé adresy URL ikony: $icons[ 'iconName' ]['url'] = '//example.org/icon.svg";

Případně můžete použít cestu k souboru, která je relativní vzhledem k $wgExtensionAssetsPath : $icons[ 'iconName' ]['path'] = 'extensionPath/to/icon.svg";

Definování informací o události

Parametr Požadované Popis
category yes Kategorie, do které tato akce patří.

To je důležité pro zobrazení na stránce Special:Preferences

section yes Definuje sekci, do které toto oznámení patří.

Existují dva typy, Alerts a Notices. Upozornění jsou pro naléhavá oznámení, která by měla být vyřízena okamžitě, např. příspěvek na diskusní stránce uživatele. Oznámení jsou pro upozornění, která nemusí být vyřízena okamžitě, např. poděkování. Pro upozornění vložte 'alert'. Pro oznámení vložte 'message'

presentation-model yes Třída pro model prezentace pro toto zobrazení události
user-locators yes Definuje funkce, které vytvářejí seznam uživatelů, kteří mají být upozorněni
group no Seskupte jej s podobnými oznámeními.

Známé skupiny v jádru Echo jsou: 'positive', 'negative', 'interactive', 'neutral'. Pokud není zadán, použije se 'neutral'.

user-filters[Pitfalls 1] no Definuje funkce, které vytvářejí seznam uživatelů, kteří nemají být upozorňováni.
immediate no Zda použít frontu úloh nebo ne.

Boolean, výchozí je použití fronty úloh (tj. false)

Podrobnosti o vytvoření události

Když vytváříme a spouštíme událost, musíme také poskytnout podrobnosti:

Parametr Požadované Popis
type yes Název události, klíč, který jsme použili k definování události přidáním k &$notifications v háčku BeforeCreateEchoEvent
title no Související název, je-li relevantní.
extra no Řada dalších definic pro Echo a model prezentace k použití
agent no Uživatel, který je původcem tohoto oznámení (poznámka: Toto není uživatel, který je upozorněn!)

Definice prezentačního modelu

Prezentační modely mají být velmi flexibilní, aby umožňovaly vytváření typů upozornění se specifickými displeji. Na rozdíl od definice a vytvoření události je prezentační model samostatnou třídou a vyžaduje jeho rozšíření a vytvoření metod specifických pro případ oznámení. Toto jsou definice základní třídy EchoEventPresentationModel:

Metoda Výchozí hodnota Popis
canRender[Pitfalls 2] true Definuje podmínky, za kterých může být toto oznámení poskytnuto.

Můžete například zkontrolovat, zda byla stránka odstraněna, a pokud ano, nezobrazovat oznámení. Běžné použití je, pokud oznámení kdekoli používá $this->title stránky (pro záhlaví zprávy nebo pro odkazy), musí canRender() zkontrolovat, zda je nastaveno $this->title.

getIconType Musí být přepsáno Definuje symbolický název ikony pro toto oznámení (jak je definováno v $wgEchoNotificationIcons)
getHeaderMessage Zpráva založená na výchozím klíči notification-header-{$this->type} Definuje zprávu i18n pro záhlaví
getCompactHeaderMessage Pokud není definován, použije getHeaderMessage() Definuje zprávu i18n pro záhlaví v případech, kdy je oznámení „kompaktní“ (například uvnitř balíčku nebo skupiny napříč wiki).
getSubjectMessage Pokud není definován, použije getHeaderMessage() Definuje zprávu i18n pro předmět e-mailu upozornění
getBodyMessage false Definuje zprávu i18n pro tělo oznámení.

Volitelné.

getPrimaryLink Musí být přepsáno Definuje primární odkaz pro toto oznámení.

Pro definici primárního odkazu použijte strukturu ['url' => (string) url, 'label' => (string) link text (non-escaped)] ('label' se používá pro verze bez JavaScriptu)

Pokud neexistuje primární odkaz, vrátí false.

getSecondaryLinks[Pitfalls 3] array(); Definuje sekundární akce

Struktura sekundárních odkazů

Informace o sekundárních odkazech umožňují oznámením specifikovat dílčí odkazy na akce, které rozšiřují hlavní primární odkaz pro oznámení. Například oznámení o nějaké úpravě revize může mít svůj primární odkaz ukazovat na stránku s rozdílem revize, ale také mít sekundární odkazy směřující na uživatelskou stránku uživatele, který provedl změnu, nebo odkaz na zobrazení stránky, která byla změněna.

Sekundární odkazy také umožňují akce řízené API (akce 'dynamic') pro akce jako 'stop watching a page' nebo jiné, které vyžadují, aby front-end požádal o požadavek AJAX na API přímo z oznámení. Další podrobnosti o dynamických akcích jsou mimo rozsah tohoto kurzu.

Obecná struktura sekundárních odkazů:

public function getSecondaryLinks() {
    return [
       [
            'url' => (string) url,
            'label' => (string) link text (non-escaped),
            'description' => (string) descriptive text (optional, non-escaped),
            'icon' => (bool|string) symbolic ooui icon name (or false if there is none),
        ],
    ];
);

Návod: Vytvoření nového typu oznámení

Řekněme, že máme rozšíření, které vede seznam uživatelů, kteří mají zájem o nové tituly vytvořené s konkrétními slovy. Rozšíření umožňuje uživatelům přidávat a odebírat se ze seznamu sledovaných pro určitý seznam slov v názvu a poslouchá háček pro vytváření nových stránek na wiki, aby identifikoval relevantní stránky. Když je vytvořena relevantní stránka, kód rozšíření vytvoří událost s relevantními podrobnostmi a upozorní uživatele, kteří jsou na seznamu pro tuto kombinaci slov.

Tato část ukáže, jak vytvořit tento nový typ oznámení.

Definování události

Musíme se ujistit, že naše událost je definována v systému. To se provádí poslechnutím háčku BeforeCreateEchoEvent a definováním definice události:

class MyExtHooks {
    ...

    public static function onBeforeCreateEchoEvent(
		&$notifications, &$notificationCategories, &$icons
	) {
	    // Define the category this event belongs to
	    // (this will appear in Special:Preferences)
	    $notificationCategories['my-ext-topic-word-follow'] = [
			'priority' => 3,
			'tooltip' => 'echo-pref-tooltip-my-ext-topic-word-follow',
		];

	    
	    // Define the event
        $notifications['my-ext-topic-word'] = [
    		'category' => 'my-ext-topic-word-follow',
    		'group' => 'positive',
    		'section' => 'alert',
    		'presentation-model' => EchoMyExtTopicWordPresentationModel::class,
    		'bundle' => [
    			'web' => true,
    			'expandable' => true,
    		],
            'user-locators' => [ 'MyExtensionClass::locateUsersInList' ],
            // We might want to add a filter to remove mentioned users here, because
            // a page may have been created with mentions - and we don't want the
            // mentioned user to be notified twice. (This is an implementation choice
            // of course - 'user-filters' is not mandatory)
            'user-filters' => [ 'MyExtensionClass::locateMentionedUsers' ]
    	];

        // Define the icon to use for this notification
        // You can use existing icons in Echo, but if the icon is
        // new, you must define it
		$icons['my-ext-topic-word'] = [
			// You can provide a url that will not be qualified
			'url' => 'http://example.org/icon.svg'
			// If a url is not given, then a Echo will look for a path
			'path' => 'MyExt/icons/TopicWordNotification.svg',
			// If neither a url or path is given then a placeholder will be used
		];
    }
    
    ...
}

Další příklad tohoto můžete vidět v Thanks extension.

Identifikujte příslušné uživatele, které chcete upozornit

Systém oznámení se pokusí upozornit relevantní uživatele, ale nemůže uhodnout, kdo to jsou. Kód, který vytváří událost, musí poskytnout tyto informace. K tomu bychom měli vytvořit funkci, která se následně vloží do definice nové události.

V tomto případě budeme chtít projít seznam uživatelů a přidat je všechny:

class MyExtensionClass {
    ...

    /**
     * @param EchoEvent $event
     * @return array
     */
    public function locateUsersInList( EchoEvent $event ) {
        // Get the list of users
        $userIds = $this->getList( $event )->getUsers();
        
        return array_map( function ( $userId ) {
            return User::newFromId( $userId );
        }, $userIds );
    }

    ...
}

Přidání kódu pro spuštění události

Nyní, když máme způsob, jak identifikovat, koho chceme upozornit, musíme tuto událost skutečně spustit. Naše hypotetické rozšíření naslouchá háčku vytváření stránky a poté zkontroluje, zda nový název obsahuje slova, která se hodí do seznamu Jakkoli to kód rozšíření dělá, jakmile identifikuje název, který se hodí do seznamu, vytvoří událost a upozorní příslušné uživatele.

Níže uvedený kód přeskočí skutečnou operaci poslechu vytváření nové stránky a kontroly slov a skočí přímo do definování nové události:

// ...
// Some code to listen to new page creation, check words
// and find the relevant list
// ...

// Creating the event
EchoEvent::create( [
	'type' => 'my-ext-topic-word',
	'title' => $title,
	'extra' => [
		'revid' => $revision->getId(),
		'source' => $source,
		'excerpt' => EchoDiscussionParser::getEditExcerpt( $revision, $this->getLanguage() ),
	],
	'agent' => $user,
] );

Příklad filtrování zmíněných uživatelů můžete vidět v události Flow pro flow-new-topic.

Vytvoření prezentačního modelu

Nyní, když je vše nastaveno, můžeme vytvořit náš prezentační model. Budeme muset rozšířit EchoEventPresentationModel a implementovat naše metody. Můžete vidět několik příkladů těchto typů modelů v několika rozšířeních, jako je Flow [1][2][3][4], Thanks a uvnitř samotného Echo [5][6][7].


Zde je příklad prezentačního modelu pro naše hypotetické rozšíření:

<?php
class EchoMyExtTopicWordPresentationModel extends EchoEventPresentationModel {
	public function canRender() {
	    // Define that we have to have the page this is
	    // refering to as a condition to display this
	    // notification
		return (bool)$this->event->getTitle();
	}
	public function getIconType() {
	    // You can use existing icons in Echo icon folder
	    // or define your own through the $icons variable
	    // when you define your event in BeforeCreateEchoEvent
		return 'someIcon';
	}
	public function getHeaderMessage() {
		if ( $this->isBundled() ) {
		    // This is the header message for the bundle that contains
		    // several notifications of this type
			$msg = $this->msg( 'notification-bundle-myext-topic-word' );
			$msg->params( $this->getBundleCount() );
			$msg->params( $this->getTruncatedTitleText( $this->event->getTitle(), true ) );
			$msg->params( $this->getViewingUserForGender() );
			return $msg;
		} else {
		    // This is the header message for individual non-bundle message
			$msg = $this->getMessageWithAgent( 'notification-myext-topic-word' );
			$msg->params( $this->getTruncatedTitleText( $this->event->getTitle(), true ) );
			$msg->params( $this->getViewingUserForGender() );
			return $msg;
		}
	}
	public function getCompactHeaderMessage() {
	    // This is the header message for individual notifications
	    // *inside* the bundle
		$msg = parent::getCompactHeaderMessage();
		$msg->params( $this->getViewingUserForGender() );
		return $msg;
	}
	public function getBodyMessage() {
	    // This is the body message.
        // We will retrieve the edit summary that we added earlier with EchoEvent::create().
		$comment = $this->event->getExtraParam( 'excerpt', false );
		if ( $comment ) {
			// Create a dummy message to contain the excerpt.
			$msg = new RawMessage( '$1' );
			$msg->plaintextParams( $comment );
			return $msg;
		}
	}
	public function getPrimaryLink() {
	    // This is the link to the new page
	    $link = $this->getPageLink( $this->event->getTitle(), '', true );
        return $link;
    }

	public function getSecondaryLinks() {
		if ( $this->isBundled() ) {
		    // For the bundle, we don't need secondary actions
			return [];
		} else {
		    // For individual items, display a link to the user
		    // that created this page
			return [ $this->getAgentLink() ];
		}
	}
}

A to je vše! Naše nové upozornění je nyní aktivní a bude se zobrazovat příslušným uživatelům s hlavičkou a tělem zpráv a primárními a sekundárními odkazy, které jsme definovali.

Jak seskupovat oznámení

  • Ujistěte se, že je konfigurace balíčku nastavena na hodnotu true v definici události echo:
    $notifications['my-ext-topic-word'] = [
    		'bundle' => [
    			'web' => true,        // Bundle when rendering on the web?
    			'email' => true,      // Bundle in emails?
    			'expandable' => true, // Make bundles expandable?
    		],
            // More stuff here
    ]
  • Přidejte háček pro pravidla sdružování:
public static function onEchoGetBundleRules( $event, &$bundleString ) {
    switch ( $event->getType() ) {
        case 'my-ext-topic-word':            // Your notification type. This
                                             // is the key you set for the
                                             // first parameter in your
                                             // BeforeCreateEchoEvent hook.
                                             
            $bundleString = 'my-ext-topic';  // Which messages go into which
                                             // bundle. For the same bundle,
                                             // return the same $bundleString.
        break;
    }
    return true;
}
  • Připojte funkci v extension.json:
    "Hooks": {
        "BeforeCreateEchoEvent": [
            "LoginNotifyHooks::onBeforeCreateEchoEvent"
        ],
        "EchoGetBundleRules": [
            "LoginNotifyHooks::onEchoGetBundleRules"
        ]
        // More stuff here
    }
  • Můžete použít $this->isBundled() v kódu svého prezentačního modelu ke kontrole, zda jste v balíčku oznámení, a podle toho upravit záhlaví a tělo zprávy:
public function getHeaderMessage() {
    if ( $this->isBundled() ) {
        $msg = $this->msg( 'something' );
        return $msg;
    } else {
        ...
    }
}
  • Když se rozbalitelný balíček rozbalí, každé dílčí oznámení se vykreslí samostatně. Ve výchozím nastavení je zobrazeno getHeaderMessage(), ale doporučuje se, abyste pro tento případ uvedli kratší zprávu do kódu prezentačního modelu pomocí getCompactHeaderMessage():
public function getCompactHeaderMessage() {
    return $this->msg( 'somethingshort' );
}

A máte hotovo! Nezapomeňte přidat své zprávy. Všimněte si také, že balíčky mohou nebo nemusí být rozšiřitelné.

Úskalí a varování

  1. Role user-locators a user-filters je důležitá, abychom zajistili, že nebudeme uživatelům z jediné události poskytovat dvojité oznámení. Například, pokud je ve Flow vytvořeno nové téma, chceme upozornit všechny, kteří sledují nástěnku – ale chceme ignorovat uživatele, kteří byli zmíněni, protože 'mention' vytvoří vlastní událost.
    Při vytváření nových událostí se snažte ujistit se, že jste odfiltrovali potenciální kolidující události pro uživatele, kteří byli upozorněni, jinak dostanou dvě oznámení pro stejnou událost.
  2. Všimněte si, že canRender je neuvěřitelně důležitá metoda. Kromě ovlivnění toho, zda se oznámení zobrazí, je to také součást testu, který systém oznámení používá k moderování oznámení. Pokud bylo například vytvořeno oznámení o nové zmínce pro uživatele, ale revize, která to vyvolala, byla poté odstraněna nebo potlačena, oznámení by mělo také zmizet. Systém zkontroluje, zda lze oznámení zobrazit (mimo jiné pomocí 'canRender') - pokud canRender v tomto případě zkontroluje, že revize existuje, vrátí 'false' (protože revize byla smazána), což zajistí, že oznámení bude označeno příznakem pro smazání. Nesprávné definování canRender může vést k chybám, protože oznámení se pokusí vyžádat podrobnosti o chybějícím názvu nebo revizi, takže pokud je to potřeba, dejte pozor, abyste je definovali.
  3. getSecondaryLinks musí vrátit pole. Pokud vaše oznámení obsahuje pouze sekundární odkazy v určitých případech, ujistěte se, že všechny ostatní případy vrací pole a ne 'false' nebo 'null'. Na rozdíl od getPrimaryLink, která vrací false pro prázdnou hodnotu, getSecondaryLinks musí vrátit pole.

Poznámky a reference