Robert Vogel has submitted this change and it was merged.

Change subject: [WIP] Notifications
......................................................................


[WIP] Notifications

* Implemented localStorage based storage mechanism for notifications
* Improved init mechanism
* Added notification badge
* Added notification flyout based on OO.ui.PopupWidget
* Added assets for button template and OPT-IN gadget
* Now using marker for base page insertion
* Built own message dialog to have anchor tags displayed

TODO
* Find reason for strange DB errors in API calls with "\n" in parameter
* Fix popup displacement

Change-Id: I525195eac98ff7addd6f47546f40d50daf51d707
---
A assets/MediaWiki_Gadget-teahouse-opt-in.wiki
A assets/MediaWiki_Gadget-teahouse-opt-in_main.js.wiki
M assets/MediaWiki_Gadget-teahouse.wiki
A assets/MediaWiki_Gadgets-definition.partial.wiki
A assets/Template_Answered_question.wiki
A assets/Template_Ask_your_question.wiki
A assets/Template_Pending_question.wiki
A assets/Template_Question.wiki
M gadgetize.json
M i18n/de.json
M i18n/en.json
M i18n/qqq.json
A resources/mediawiki.teahouse.board.js
M resources/mediawiki.teahouse.css
M resources/mediawiki.teahouse.dialog.js
M resources/mediawiki.teahouse.gadget.development.js
M resources/mediawiki.teahouse.js
A resources/mediawiki.teahouse.notifications.js
A resources/ui/dialogs/th.ui.MessageDialog.js
M resources/ui/dialogs/th.ui.QuestionDialog.js
20 files changed, 663 insertions(+), 128 deletions(-)

Approvals:
  Robert Vogel: Verified; Looks good to me, approved



diff --git a/assets/MediaWiki_Gadget-teahouse-opt-in.wiki 
b/assets/MediaWiki_Gadget-teahouse-opt-in.wiki
new file mode 100644
index 0000000..24bc039
--- /dev/null
+++ b/assets/MediaWiki_Gadget-teahouse-opt-in.wiki
@@ -0,0 +1 @@
+[[Wikipedia:Teahouse|Wikipedia Teahouse]] OPT IN - Always display "Ask a 
question" link in personal tools
\ No newline at end of file
diff --git a/assets/MediaWiki_Gadget-teahouse-opt-in_main.js.wiki 
b/assets/MediaWiki_Gadget-teahouse-opt-in_main.js.wiki
new file mode 100644
index 0000000..d8a96fd
--- /dev/null
+++ b/assets/MediaWiki_Gadget-teahouse-opt-in_main.js.wiki
@@ -0,0 +1 @@
+/* This is just a dummy script that allows to have the "Teahouse-opt-in" 
gadget in the preferences */
\ No newline at end of file
diff --git a/assets/MediaWiki_Gadget-teahouse.wiki 
b/assets/MediaWiki_Gadget-teahouse.wiki
index 7391e1e..911539a 100644
--- a/assets/MediaWiki_Gadget-teahouse.wiki
+++ b/assets/MediaWiki_Gadget-teahouse.wiki
@@ -1 +1 @@
-Wikipedia Teahouse - Wikimedia Deutschland e.V., Hallo Welt! - Medienwerkstatt 
GmbH, [[mw:User:Osnard|Robert Vogel]]
\ No newline at end of file
+[[Wikipedia:Teahouse|Wikipedia Teahouse]] BASE - Wikimedia Deutschland e.V., 
Hallo Welt! - Medienwerkstatt GmbH, Author: [[mw:User:Osnard|Robert Vogel]]
\ No newline at end of file
diff --git a/assets/MediaWiki_Gadgets-definition.partial.wiki 
b/assets/MediaWiki_Gadgets-definition.partial.wiki
new file mode 100644
index 0000000..5f7b5f8
--- /dev/null
+++ b/assets/MediaWiki_Gadgets-definition.partial.wiki
@@ -0,0 +1,4 @@
+<!-- The following lines have to be added to "MediaWiki:Gadgets-definition" -->
+
+* teahouse[ResourceLoader|default]|teahouse/main.js|teahouse/teahouse.css
+* teahouse-opt-in[ResourceLoader]|teahouse/main.js
\ No newline at end of file
diff --git a/assets/Template_Answered_question.wiki 
b/assets/Template_Answered_question.wiki
new file mode 100644
index 0000000..817429f
--- /dev/null
+++ b/assets/Template_Answered_question.wiki
@@ -0,0 +1,3 @@
+<includeonly>
+[[Category:Teahouse/Answered_Question]]
+</includeonly>
\ No newline at end of file
diff --git a/assets/Template_Ask_your_question.wiki 
b/assets/Template_Ask_your_question.wiki
new file mode 100644
index 0000000..7528ba6
--- /dev/null
+++ b/assets/Template_Ask_your_question.wiki
@@ -0,0 +1 @@
+<span class="th-ask" style="display: inline-block; background-color: #27AA65; 
padding: 0.5em; color:#F0F0F0; cursor:pointer">Ask your question</span>
\ No newline at end of file
diff --git a/assets/Template_Pending_question.wiki 
b/assets/Template_Pending_question.wiki
new file mode 100644
index 0000000..9394d60
--- /dev/null
+++ b/assets/Template_Pending_question.wiki
@@ -0,0 +1,3 @@
+<includeonly>
+[[Category:Teahouse/Pending_Question]]
+</includeonly>
\ No newline at end of file
diff --git a/assets/Template_Question.wiki b/assets/Template_Question.wiki
new file mode 100644
index 0000000..0bbf46c
--- /dev/null
+++ b/assets/Template_Question.wiki
@@ -0,0 +1,5 @@
+<noinclude>
+This is the wrapper template that embeds a question into the question list
+</noinclude><includeonly><h3>[[Project:Teahouse/Questions/{{{title}}}|{{{title}}}]]</h3>
+{{Project:Teahouse/Questions/{{{title}}}}}
+</includeonly>
\ No newline at end of file
diff --git a/gadgetize.json b/gadgetize.json
index 08b15a9..dbebc85 100644
--- a/gadgetize.json
+++ b/gadgetize.json
@@ -1,5 +1,7 @@
 {
        "MediaWiki:Gadget-teahouse": ["assets/MediaWiki_Gadget-teahouse.wiki"],
+       "MediaWiki:Gadget-teahouse-opt-in": 
["assets/MediaWiki_Gadget-teahouse-opt-in.wiki"],
+       "MediaWiki:Gadget-teahouse-opt-in/main.js": 
["assets/MediaWiki_Gadget-teahouse-opt-in_main.js.wiki"],
 
        "MediaWiki:Gadget-teahouse/main.js": [
                "resources/mediawiki.teahouse.js",
@@ -12,14 +14,17 @@
        "MediaWiki:Gadget-teahouse/ui/dialogs/th.ui.QuestionDialog.js": [
                "resources/ui/dialogs/th.ui.QuestionDialog.js"
        ],
+       "MediaWiki:Gadget-teahouse/ui/dialogs/th.ui.MessageDialog.js": [
+               "resources/ui/dialogs/th.ui.MessageDialog.js"
+       ],
 
        "MediaWiki:Gadget-teahouse/teahouse.css": [
                "resources/mediawiki.teahouse.css"
        ],
 
-       "Template:Teahouse/Pending_Question":  [ 
"assets/Template_Pending_Questions.wiki" ],
-       "Template:Teahouse/Answered_Question": [ 
"assets/Template_Answered_Questions.wiki" ],
-       "Template:Teahouse/Question_ListItem": [ 
"assets/Template_Answered_Questions.wiki" ],
-       "Module:Teahouse_Questions":  [ "assets/Module_Teahouse_Questions.lua" 
],
+       "Template:Teahouse/Ask_your_question":  [ 
"assets/Template_Ask_your_question.wiki" ],
+       "Template:Teahouse/Pending_question":  [ 
"assets/Template_Pending_question.wiki" ],
+       "Template:Teahouse/Answered_question": [ 
"assets/Template_Answered_question.wiki" ],
+       "Template:Teahouse/Question": [ "assets/Template_Question.wiki" ],
        "Help:Teahouse": [ "assets/Help_Teahouse.wiki" ]
 }
\ No newline at end of file
diff --git a/i18n/de.json b/i18n/de.json
index 3c1edf0..73aeb75 100644
--- a/i18n/de.json
+++ b/i18n/de.json
@@ -6,10 +6,12 @@
        },
        "th-desc": "Entwicklungserweiterung für das 'Teahouse' Gadget",
        "th-button-text": "Stelle deine Frage",
+       "th-button-title": "Lerne die Community kennen",
        "th-dialog-title": "Stelle deine Frage",
        "th-dialog-description-top": "Stelle deine Frage an die Wikipedia 
Community! Auf $1 kannst du dir die Fragen anderer Benutzer ansehen.",
        "th-dialog-label-summary": "Deine Frage",
        "th-dialog-label-text": "Genauere Beschreibung (Du kannst WikiText 
verwenden)",
+       "th-dialog-label-similar": "Ähnliche, bereits gestellte Fragen",
        "th-dialog-btn-ok": "Veröffentlichen",
        "th-dialog-btn-cancel": "Abbrechen",
        "th-dialog-disclaimer": "Veröffentlichung unter den Nutzungsbedingungen 
der Wikipedia",
@@ -17,5 +19,7 @@
        "th-dialog-msg-title-save": "Frage veröffentlicht",
        "th-dialog-msg-text-save": "Deine Frage wurde unter $1 veröffentlicht. 
Möchtest du die komplette Liste der Fragen sehen?",
        "th-dialog-msg-btn-yes": "Ja",
-       "th-dialog-msg-btn-no": "Nein"
+       "th-dialog-msg-btn-no": "Nein",
+       "th-notifications-badge-title": "Es gibt Reaktionen auf deine Fragen",
+       "th-notifications-popup-title": "Reaktionen auf deine Fragen"
 }
diff --git a/i18n/en.json b/i18n/en.json
index 7faee83..3c796cd 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -6,10 +6,12 @@
        },
        "th-desc": "Development extension for \"Teahouse\" gadget",
        "th-button-text": "Ask your question",
+       "th-button-title": "Get in touch with the community",
        "th-dialog-title": "Ask your question",
        "th-dialog-description-top": "Ask your question to the Wikipedia 
Community! You can see the questions of other users on $1",
        "th-dialog-label-summary": "Your question",
        "th-dialog-label-text": "Further description (You may use wikitext)",
+       "th-dialog-label-similar": "Similar questions",
        "th-dialog-btn-ok": "Publish",
        "th-dialog-btn-cancel": "Cancel",
        "th-dialog-disclaimer": "Published under Wikipedia's Terms of Use",
@@ -17,5 +19,7 @@
        "th-dialog-msg-title-save": "Question published",
        "th-dialog-msg-text-save": "Your question has been published on $1. Do 
you wand to see the complete list of questions?",
        "th-dialog-msg-btn-yes": "Yes",
-       "th-dialog-msg-btn-no": "No"
+       "th-dialog-msg-btn-no": "No",
+       "th-notifications-badge-title": "There are reactions to your questions",
+       "th-notifications-popup-title": "Reactions to your questions"
 }
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 2030733..36c825e 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -6,12 +6,20 @@
        },
        "th-desc": "Used in [[Special:Version]], description teahouse 
extension",
        "th-button-text": "Text of a button/link on a page and within the user 
menu",
+       "th-button-title": "Title attribute of button anchor",
        "th-dialog-title": "Title of the dialog box",
        "th-dialog-description-top": "A short description of how to use teh 
dialog at the beginning",
        "th-dialog-label-summary": "Label of a text field that contains the 
question itself",
        "th-dialog-label-text": "Label of a textarea that allows an additional 
information for the question",
+       "th-dialog-label-similar": "Label of a list with links to similar 
questions",
        "th-dialog-btn-ok": "Text of the button that sends the question. It 
should be clear that the action makes the question available to the public.",
        "th-dialog-btn-cancel": "Text of the button that resets the form and 
closes the dialog",
        "th-dialog-disclaimer": "Hint to the Terms of Use. Links to 
https://wikimediafoundation.org/wiki/Terms_of_Use";,
-       "th-dialog-anon-ip-hint": "Hint that the IP adress of an anonymous user 
will be saved"
+       "th-dialog-anon-ip-hint": "Hint that the IP adress of an anonymous user 
will be saved",
+       "th-dialog-msg-title-save": "",
+       "th-dialog-msg-text-save": "",
+       "th-dialog-msg-btn-yes": "",
+       "th-dialog-msg-btn-no": "",
+       "th-notifications-badge-title": "",
+       "th-notifications-popup-title": ""
 }
diff --git a/resources/mediawiki.teahouse.board.js 
b/resources/mediawiki.teahouse.board.js
new file mode 100644
index 0000000..a3ce14f
--- /dev/null
+++ b/resources/mediawiki.teahouse.board.js
@@ -0,0 +1,162 @@
+(function( mw, $, d, undefined ){
+
+       function _publishQuestion( question ) {
+               //We append a signature WikiText fragment by default
+               var signature = ['-', '-~~', '~~'].join(''); //This is just to 
prevent the wikitext parser from parsing it on deployment
+               if( question.text.indexOf( signature ) === -1 ) {
+                       question.text += "\n\n" + signature;
+               }
+
+               question.text += "\n\n{{" + _config.templates.pendingQuestion + 
'}}';
+
+               var dfd = $.Deferred();
+
+               //Step 1: Create the question subpage
+               var createQuestionAPI = new mw.Api();
+               createQuestionAPI.postWithToken( 'edit', {
+                       action: 'edit',
+                       watchlist: 'watch',
+                       title: _config.basePage + "/" + question.title,
+                       summary: "Added by Teahouse gadget",
+                       text: question.text
+               })
+               .fail(function( resp1, jqXHR ){
+                       console.log( resp1 );
+                       dfd.reject();
+               })
+               .done(function( resp1, jqXHR ){
+                       //Step 2: From the response build the template text for 
the board
+                       var title = resp1.edit.title + '';
+                       var timestamp = resp1.edit.newtimestamp;
+                       var wrapper = _config.templates.questionWrapper + '';
+                       mw.teahouse.notifications.registerTitle( title, 
timestamp );
+                       var titleParts = title.split( '/' );
+                       var basename = titleParts[titleParts.length-1];
+
+                       var template = "\n" + '{{' + wrapper + '|title=' + 
basename + '}}'+"\n";
+
+                       //Step 3: Query the WikiText content of the board
+                       var getBasePageWikiTextAPI = new mw.Api();
+                       getBasePageWikiTextAPI.get({
+                               action: 'query',
+                               titles: _config.basePage,
+                               prop: 'revisions',
+                               rvprop: 'content',
+                               indexpageids : ''
+                       })
+                       .fail(function( resp2, jqXHR ){
+                               console.log( resp2 );
+                               dfd.reject();
+                       })
+                       .done(function( resp2, jqXHR ){
+                               //Step 4: Add the new template at the desired 
position within
+                               //the board page
+                               var pageId = resp2.query.pageids[0];
+                               var content = 
resp2.query.pages[pageId].revisions[0]['*'];
+                               var contentParts = content.split( 
_config.insertMarker, 2 );
+                               if( contentParts.length === 1 ) { //No 
_config.insertMarker found
+                                       contentParts[0] += "\n";
+                                       contentParts.push(''); //We append an 
empty string
+                               }
+
+                               contentParts[1] = template + contentParts[1];
+                               content = contentParts.join( 
_config.insertMarker );
+
+                               //Step 5: Write the new content to the board 
page
+                               var addToListAPI = new mw.Api();
+                               addToListAPI.postWithToken( 'edit', {
+                                       action: 'edit',
+                                       title: _config.basePage,
+                                       text: content,
+                                       summary: "Added by Teahouse gadget"
+                               })
+                               .fail(function( resp3, jqXHR ){
+                                       console.log( resp3 );
+                                       dfd.reject();
+                               })
+                               .done(function( resp3, jqXHR ){
+                                       dfd.resolve( { questionpage: resp1.edit 
} );
+                               });
+                       });
+               });
+
+               return dfd.promise();
+       }
+
+       var _sqCache = {};
+       var _lastPromise = undefined;
+       function _getSimilarQuestions( value, $target, callback ) {
+               mw.loader.using( 'mediawiki.Title', function() {
+                       var cacheKey = value.toLowerCase();
+                       var baseTitle = new mw.Title( _config.basePage );
+                       var prefix = baseTitle.getPrefixedText() + '/';
+
+                       if( cacheKey in _sqCache ) {
+                               _renderSimilarQuestionsList( 
_sqCache[cacheKey], prefix, $target );
+                               callback();
+                               return;
+                       }
+
+                       if( _lastPromise ) {
+                               _lastPromise.abort();
+                       }
+
+                       var _api = new mw.Api();
+                       _lastPromise = _api.get({
+                               action: 'query',
+                               list: 'search',
+                               srsearch: value,
+                               srlimit: 50,
+                               srnamespace: baseTitle.getNamespaceId()
+                       })
+                       .done(function( response, jqXHR ){
+                               _sqCache[cacheKey] = response.query.search;
+                               _renderSimilarQuestionsList( 
response.query.search, prefix, $target );
+                               callback();
+                               _lastPromise = undefined;
+                       });
+
+               });
+       }
+
+       function _renderSimilarQuestionsList( items, prefix, $target ) {
+               var $list = $('<ul>').addClass('th-similar-questions-list');
+               var count = 0;
+
+               for( var i = 0; i < items.length && count < 5; i++ ) {
+                       var title = items[i].title;
+                       if( title.indexOf( prefix ) !== 0 ) {
+                               continue;
+                       }
+
+                       var displayTitle = title.replace( prefix, '' );
+                       var anchor =  mw.html.element( 'a', {
+                               href: mw.util.getUrl( title ),
+                               title: title,
+                               target: '_blank'
+                       }, displayTitle );
+                       $list.append( '<li>'+anchor+'</li>' );
+                       count++;
+               }
+               $target.empty();
+               if( count > 0 ) {
+                       var $label = $('<span>').addClass( 
'oo-ui-labelElement-label' )
+                               .append( 
mw.message('th-dialog-label-similar').plain() );
+                       $target.append( $label );
+                       $target.append( $list );
+               }
+       }
+
+       var _config = {};
+       function _init( config ) {
+               delete( mw.teahouse.board.init );
+               _config = config;
+       }
+       //TODO: Make mw.teahouse.board a OOJS object of class "th.Board"
+       mw.teahouse.board = {
+               init: _init,
+               publishQuestion: _publishQuestion,
+               getSimilarQuestions: _getSimilarQuestions
+       };
+
+})( mediaWiki, jQuery, document );
\ No newline at end of file
diff --git a/resources/mediawiki.teahouse.css b/resources/mediawiki.teahouse.css
index 8434c3c..15ac186 100644
--- a/resources/mediawiki.teahouse.css
+++ b/resources/mediawiki.teahouse.css
@@ -3,6 +3,11 @@
        color: #F0F0F0;
        background-color: #27AA65; /* 
https://www.mediawiki.org/wiki/Wikimedia_Foundation_Design/Color_usage */
        padding: 0 0.5em;
+       text-decoration: none;
+}
+
+.th-question-dialog-body .oo-ui-labelElement-label {
+       font-weight: bold;
 }
 
 .th-question-dialog-body .th-inputWidget {
@@ -17,6 +22,35 @@
        cursor: initial;
 }
 
-.th-question-dialog-body .th-similar-questions-list {
-       list-style-type: none;
+.th-question-dialog-body .th-similar-questions-list,
+.th-notif-popup-content .th-notif-popup-list {
+       list-style: none;
+       margin: 0;
+}
+
+#p-personal .th-notif-popup-content .th-notif-popup-list li {
+       float: none;
+       font-size: 100%;
+}
+
+#p-personal .th-notif-popup-content .th-notif-popup-list {
+       padding: 0.5em;
+}
+
+/* Borrowed from Echo extension */
+a.th-notifications-badge {
+       min-width: 7px;
+       border-radius: 2px;
+       padding: 0.25em 0.45em 0.2em 0.45em;
+       margin-left: -4px;
+       text-align: center;
+       /*background-color: #d2d2d2;*/
+       font-weight: bold;
+       color: white;
+       cursor: pointer;
+       text-decoration: none;
+}
+
+a.th-unread-notifications {
+       background-color: #27AA65;
 }
\ No newline at end of file
diff --git a/resources/mediawiki.teahouse.dialog.js 
b/resources/mediawiki.teahouse.dialog.js
index 9c3497d..6f4934a 100644
--- a/resources/mediawiki.teahouse.dialog.js
+++ b/resources/mediawiki.teahouse.dialog.js
@@ -1,51 +1,89 @@
 (function( mw, $, d, undefined ){
+       function _getComponentUrl( path ) {
+               var url = new mw.Uri( _config.resourcesPath + path );
+               url.query['action'] = 'raw';
+               url.query['ctype'] = 'text/javascript';
 
-       function _setupDialog( config ) {
+               return url.toString();
+       }
 
+       function _getWindowManager() {
+               if( !_windowManager ) {
                        _windowManager = new OO.ui.WindowManager({
                                modal: true,
                                isolate: true
                        });
                        $( 'body' ).append( _windowManager.$element );
+               }
+               return _windowManager;
+       }
 
-                       _window = new th.ui.QuestionDialog( config );
-                       _messageDialog = new OO.ui.MessageDialog();
-                       _windowManager.addWindows( [ _window, _messageDialog ] 
);
+       function _setupQuestionDialog() {
+               _questionDialog = new th.ui.QuestionDialog( {}, _config );
+               _getWindowManager().addWindows( [ _questionDialog ] );
+       }
 
-                       return;
+       function _setupMessageDialog() {
+               _messageDialog = new th.ui.MessageDialog( {}, _config );
+               _getWindowManager().addWindows( [ _messageDialog ] );
        }
 
        var _windowManager = undefined;
-       var _window = undefined;
+       var _questionDialog = undefined;
        var _messageDialog = undefined;
 
-       mw.teahouse.openQuestionDialog = function( data ) {
+
+       function _openQuestionDialog( data ) {
 
                mw.loader.using( ['oojs-ui', 'mediawiki.Uri'], function(){
-                       var config = mw.teahouse.getConfig();
+                       if( !_questionDialog ) {
+                               $.getScript( _getComponentUrl( 
"/ui/dialogs/th.ui.QuestionDialog.js" ), function(){
+                                       _setupQuestionDialog();
+                                       mw.teahouse.dialog.openQuestionDialog( 
data ); //re-call after dependency is loaded
+                               });
+                               return false;
+                       }
 
-                       if( !_windowManager ) {
-                               var url = new mw.Uri( config.resourcesPath + 
"/ui/dialogs/th.ui.QuestionDialog.js" );
-                               url.query['action'] = 'raw';
-                               url.query['ctype'] = 'text/javascript';
+                       data = $.extend( data, {
+                               config: _config
+                       });
+                       _windowManager.openWindow( _questionDialog, data );
+               });
 
-                               $.getScript( url.toString(), function(){
-                                       _setupDialog();
-                                       mw.teahouse.openQuestionDialog( data ); 
//re-call after dependency is loaded
+               return false;
+       }
+
+       function _openMessageDialog( data, then ) {
+
+               mw.loader.using( ['oojs-ui', 'mediawiki.Uri'], function(){
+                       if( !_messageDialog ) {
+                               $.getScript( _getComponentUrl( 
"/ui/dialogs/th.ui.MessageDialog.js" ), function(){
+                                       _setupMessageDialog();
+                                       mw.teahouse.dialog.openMessageDialog( 
data, then ); //re-call after dependency is loaded
                                });
                                return;
                        }
 
-                       data = $.extend( data, {
-                               config: config,
-                               board: mw.teahouse.board
-                       });
-                       _windowManager.openWindow( _window, data );
+                       _windowManager
+                               .openWindow( _messageDialog, data )
+                               .then(then);
                });
+       }
+
+       var _config = {};
+       function _init( config ) {
+               delete(mw.teahouse.dialog.init);
+               _config = config;
+
+               //Register event handler for click on ...
+               $(d).on( 'click', '#p-teahouse', 
mw.teahouse.dialog.openQuestionDialog ); //... menu link
+               $(d).on( 'click', '.th-ask', 
mw.teahouse.dialog.openQuestionDialog ); //... custom element
+       }
+
+       mw.teahouse.dialog = {
+               init: _init,
+               openQuestionDialog: _openQuestionDialog,
+               openMessageDialog: _openMessageDialog
        };
 
-       mw.teahouse.openMessageDialog = function( data ) {
-               //TODO: make sure _windowManager is available...
-               return _windowManager.openWindow( _messageDialog, data );
-       };
 })( mediaWiki, jQuery, document );
diff --git a/resources/mediawiki.teahouse.gadget.development.js 
b/resources/mediawiki.teahouse.gadget.development.js
index 251129a..22a75fc 100644
--- a/resources/mediawiki.teahouse.gadget.development.js
+++ b/resources/mediawiki.teahouse.gadget.development.js
@@ -2,10 +2,12 @@
        basePage: 'Projekt:Teehaus/Fragen',
        i18n: {
                "th-button-text": "Stelle deine Frage",
+               "th-button-title": "Lerne die Community kennen",
                "th-dialog-title": "Stelle deine Frage",
                "th-dialog-description-top": "Stelle deine Frage an die 
Wikipedia Community! Auf $1 kannst du dir die Fragen anderer Benutzer ansehen.",
                "th-dialog-label-summary": "Deine Frage",
                "th-dialog-label-text": "Genauere Beschreibung (Du kannst 
WikiText verwenden)",
+               "th-dialog-label-similar": "Ähnliche, bereits gestellte Fragen",
                "th-dialog-btn-ok": "Veröffentlichen",
                "th-dialog-btn-cancel": "Abbrechen",
                "th-dialog-disclaimer": "Veröffentlichung unter den 
Nutzungsbedingungen der Wikipedia",
@@ -13,7 +15,9 @@
                "th-dialog-msg-title-save": "Frage veröffentlicht",
                "th-dialog-msg-text-save": "Deine Frage wurde unter $1 
veröffentlicht. Möchtest du die komplette Liste der Fragen sehen?",
                "th-dialog-msg-btn-yes": "Ja",
-               "th-dialog-msg-btn-no": "Nein"
+               "th-dialog-msg-btn-no": "Nein",
+               "th-notifications-badge-title": "Es gibt Reaktionen auf deine 
Fragen",
+               "th-notifications-popup-title": "Reaktionen auf deine Fragen"
        },
        resourcesPath: mw.config.get('wgScriptPath') + 
'/extensions/Teahouse/resources'
 });
\ No newline at end of file
diff --git a/resources/mediawiki.teahouse.js b/resources/mediawiki.teahouse.js
index 22d8097..d2ed025 100644
--- a/resources/mediawiki.teahouse.js
+++ b/resources/mediawiki.teahouse.js
@@ -5,19 +5,23 @@
         * single wikipedias
         */
        var _config = {
-               basePage: 'Wikipedia:Teahouse',
+               basePage: 'Wikipedia:Teahouse/Questions',
+               insertMarker: '<!-- INSERTMARKER -->',
                resourcesPath: mw.config.get('wgScript') + 
"?title=MediaWiki:Gadget-teahouse",
                templates: {
-                       pendingQuestion: 'Pending_question',
-                       answeredQuestion: 'Answered_question',
-                       questionWrapper: 'Wikipedia/Teahouse/Question'
+                       pendingQuestion: 'Teahouse/Pending_question',
+                       answeredQuestion: 'Teahouse/Answered_question',
+                       questionWrapper: 'Teahouse/Question',
+                       insertionMarker: 'Teahouse/Insertion_marker'
                },
                i18n: {
                        "th-button-text": "Ask your question",
+                       "th-button-title": "Get in touch with the community",
                        "th-dialog-title": "Ask your question",
                        "th-dialog-description-top": "Ask your question to the 
Wikipedia Community! You can see the questions of other users on $1",
                        "th-dialog-label-summary": "Your question",
                        "th-dialog-label-text": "Further description (You may 
use wikitext)",
+                       "th-dialog-label-similar": "Similar questions",
                        "th-dialog-btn-ok": "Publish",
                        "th-dialog-btn-cancel": "Cancel",
                        "th-dialog-disclaimer": "Published under Wikipedia's 
Terms of Use",
@@ -25,20 +29,22 @@
                        "th-dialog-msg-title-save": "Question published",
                        "th-dialog-msg-text-save": "Your question has been 
published on $1. Do you wand to see the complete list of questions?",
                        "th-dialog-msg-btn-yes": "Yes",
-                       "th-dialog-msg-btn-no": "No"
+                       "th-dialog-msg-btn-no": "No",
+                       "th-notifications-badge-title": "There are reactions to 
your questions",
+                       "th-notifications-popup-title": "Reactions to your 
questions"
                }
        };
 
        /**
         * Initializes the Teahouse gadget components.
-        * @param object config
         * @returns void
         */
-       function _init( config ){
-
-               //Register event handler for click on ...
-               $(d).on( 'click', '#p-teahouse', mw.teahouse.openQuestionDialog 
); //... menu link
-               $(d).on( 'click', '.th-ask', mw.teahouse.openQuestionDialog ); 
//... custom element
+       function _init(){
+               delete(mw.teahouse.init); //Can be calles only once
+               //Init sub-components and share current config with them
+               mw.teahouse.board.init( _config );
+               mw.teahouse.notifications.init( _config );
+               mw.teahouse.dialog.init( _config );
 
                //Set cookie when anon clicks edit link
                $(d).on( 'click', '#ca-ve-edit, #ca-edit', function(){
@@ -53,22 +59,27 @@
         * @returns void
         */
        function _checkUserIsEligible() {
+               //If the Teahouse-opt-in gadget is enabled we don't do any 
further checks
+               if( mw.user.options.get( 'gadget-teahouse-opt-in', 0 ) === '1' 
) {
+                       _showDialogLink();
+                       return;
+               }
                //If not logged in we just check if the user has already made 
an edit
                if( mw.user.isAnon() === true && 
mw.cookie.get('mediaWiki.teahouse.anonEdit') === '1' ) {
-                       _showLink();
+                       _showDialogLink();
                }
                //In case of a registered user we need to check for 
"autoconfirmed" group.
                else if( mw.user.isAnon() === false ) {
                        mw.user.getGroups()
                                .done(function(result){
                                        if( result[0] && $.inArray( 
'autoconfirmed', result[0] ) ) {
-                                               _showLink();
+                                               _showDialogLink();
                                        }
                                });
                }
        }
 
-       function _showLink() {
+       function _showDialogLink() {
                var linkMarkup =
                        '<li id="p-teahouse">'
                        + '<a title="' + mw.message('th-button-title').plain() 
+ '" href="#">'
@@ -76,7 +87,7 @@
                        + '</a>'
                        + '</li>';
 
-               $(linkMarkup).prependTo( $('#p-personal > ul').first() );
+               _config._$dlgLink = $(linkMarkup).prependTo( $('#p-personal > 
ul').first() );
        }
 
        mw.teahouse = {
@@ -85,14 +96,15 @@
                        mw.messages.set( _config.i18n );
                        _config = $.extend( _config, config );
                        mw.messages.set( _config.i18n );
-                       mw.loader.using( 'mediawiki.user', _init );
-               },
 
-               getConfig: function() {
-                       return _config;
+                       //TDOD: Maybe use these as Gadget dependencies within 
MediaWiki:Gadgets-definition?
+                       mw.loader.using( ['mediawiki.user', 
'mediawiki.cookie'], function() {
+                               _init.call(mw.teahouse);
+                       });
                }
        };
 
+       //register "th.ui" namespace for OOJS UI components
        window.th = {
                ui: {}
        };
diff --git a/resources/mediawiki.teahouse.notifications.js 
b/resources/mediawiki.teahouse.notifications.js
new file mode 100644
index 0000000..75c30c3
--- /dev/null
+++ b/resources/mediawiki.teahouse.notifications.js
@@ -0,0 +1,233 @@
+(function( mw, $, d, undefined ){
+       /**
+        * Just a little helper to geht the numer of properties in an object
+        * @param obj {Object} the JavaScript object
+        * @returns {Number}
+        */
+       function _objLength( obj ) {
+               var count = 0;
+               for( var key in obj ) {
+                       count++;
+               }
+               return count;
+       }
+
+       /**
+        * Saves current data for localStore
+        * @param {object} data
+        * @returns {undefined}
+        */
+       function _persistData( data ) {
+               var storageData = JSON.stringify( data || _data );
+
+               if( window.localStorage ) {
+                       window.localStorage.setItem( _storageKey, storageData );
+               }
+               else {
+                       mw.cookie.set( _storageKey, storageData );
+               }
+
+       }
+
+       /**
+        * Calls MW API, updates internal data and repeats this periodically
+        * @returns {undefined}
+        */
+       function _checkForNotifications() {
+               mw.teahouse.notifications.getCurrentNotifications()
+                       .done(function( titles ) {
+                               if( _objLength(titles) > 0 ) {
+                                       _showNoficationsLink( 
_objLength(titles) );
+                               }
+                               window.setTimeout( _checkForNotifications, 5 * 
60 * 1000 );
+                       });
+       }
+
+       var _$notif = null;
+       function _showNoficationsLink( count ) {
+               if ( !_$notif ) {
+                       var notifMarkup =
+                               '<li id="p-teahouse-notif">'
+                               + '<a class="th-notifications-badge 
th-unread-notifications" title="' + 
mw.message('th-notifications-badge-title').plain() + '" href="#">'
+                               + count
+                               + '</a>'
+                               + '</li>';
+                       _$notif = $(notifMarkup);
+
+                       //As showing the "Ask your question" link depends on an 
AJAX call
+                       //there stands the chance that this call is before or 
after the
+                       //link has been added to the DOM. We have to make sure 
the order of
+                       //elements is always the same
+                       if( _config._$dlgLink ) {
+                               _config._$dlgLink.after( _$notif );
+                       }
+                       else {
+                               _$notif.prependTo( $('#p-personal > 
ul').first() );
+                       }
+               }
+
+               _$notif.find( 'a.th-notifications-badge' ).html( count );
+       }
+
+       /**
+        * We don't use a OO.ui.PopupElement here to prevent a dependency to
+        * 'oojs-ui' module during normal page load
+        * @type OO.ui.PopupWidget
+        */
+       var _popUpWidget = null;
+       var _$popUpContent = null;
+       function _toggleNotifPopup( event ) {
+               if( _popUpWidget ) {
+                       var list = $('<ul>').addClass('th-notif-popup-list');
+                       for( var title in _data.notifications ) {
+                               var titleParts = title.split('/');
+
+                               //basename of subpage
+                               var displayTitle = 
titleParts[titleParts.length-1];
+
+                               var anchor = mw.html.element('a', {
+                                       href: mw.util.getUrl( title ),
+                                       title: title,
+                                       target: '_blank'
+                               }, displayTitle );
+                               list.append('<li>'+anchor+'</li>');
+                       }
+
+                       _$popUpContent.empty().append( list );
+                       _popUpWidget.toggle();
+               }
+               else {
+                       //Just create elements ...
+                       mw.loader.using( ['oojs-ui'], function() {
+                               _$popUpContent = $('<div>')
+                                       .addClass( 'th-notif-popup-content' );
+
+                               _popUpWidget = new OO.ui.PopupWidget({
+                                       autoClose: true,
+                                       head: true,
+                                       label: 
mw.message('th-notifications-popup-title').plain(),
+                                       $content: _$popUpContent
+                               });
+                               _$notif.append( _popUpWidget.$element );
+
+                               //... and call yourself again to populate list
+                               _toggleNotifPopup();
+                       });
+               }
+
+               return false;
+       }
+
+       var _config = {};
+       var _data = {
+               watchlist: {},
+               notifications: {},
+               lastCheck: 0
+       };
+       var _storageKey = 'mediaWiki.teahouse.notifications.data';
+       function _init( config ) {
+               delete(mw.teahouse.notifications.init);
+
+               _config = config;
+
+               var storageData = '{}';
+               if( window.localStorage ) {
+                       storageData = window.localStorage.getItem( _storageKey 
);
+               }
+               else {
+                       storageData = mw.cookie.get( _storageKey, undefined, 
'{}' );
+               }
+
+               var data = JSON.parse( storageData );
+
+               _data = $.extend( _data , data ); //Set internal data
+
+               //mark current title as read
+               mw.teahouse.notifications.markTitleAsRead( 
mw.config.get('wgPageName') );
+
+               _checkForNotifications();
+
+               $(d).on( 'click', '#p-teahouse-notif', _toggleNotifPopup );
+       }
+
+       mw.teahouse.notifications = {
+               init: _init,
+               registerTitle: function( title, timestamp ) {
+                       _data.watchlist[title] = timestamp;
+                       _persistData();
+               },
+
+               removeTitle: function( title ) {
+                       if( title in _data.watchlist ) {
+                               delete( _data.watchlist[title] );
+                       }
+                       _persistData();
+               },
+
+               markTitleAsRead: function( title ) {
+                       var t = title.replace( /_/g, ' ' );
+                       if( t in _data.watchlist ) {
+                               _data.watchlist[t] = (new Date()).toISOString();
+
+                               //Do a API call for changes
+                               _data.lastCheck = (new Date( 0 )).toISOString();
+                       }
+
+                       _persistData();
+               },
+
+               getCurrentNotifications: function() {
+                       var dfd = $.Deferred();
+                       var titles = [];
+                       for( var title in _data.watchlist ) {
+                               titles.push( title );
+                       }
+                       if( titles.length === 0 ) {
+                               dfd.reject();
+                               return dfd.promise();
+                       }
+
+                       //If the last call is less than five minutes old, we do 
not call
+                       //again, even on new page load
+                       var lastCheckPlusWait = new Date( _data.lastCheck );
+                       
lastCheckPlusWait.setMinutes(lastCheckPlusWait.getMinutes() + 5);
+
+                       if( lastCheckPlusWait > new Date() ) {
+                               dfd.resolve( _data.notifications );
+                               return dfd.promise();
+                       }
+
+                       var currentUsername = mw.user.getName();
+
+                       var api = new mw.Api();
+                       api.get({
+                               action: 'query',
+                               prop: 'revisions',
+                               titles: titles.join( '|' ),
+                               rvprop: 'timestamp|user'
+                       })
+                       .done(function( response, jqXHR ) {
+                               _data.notifications = {}; //Reset
+
+                               for( var pageId in response.query.pages ) {
+                                       var title = 
response.query.pages[pageId].title;
+                                       var revisions = 
response.query.pages[pageId].revisions;
+                                       if( !revisions ) {
+                                               continue;
+                                       }
+                                       var myDate = new Date( 
_data.watchlist[title] );
+                                       var theirDate = new Date( 
revisions[0].timestamp );
+
+                                       if( theirDate > myDate && 
currentUsername !== revisions[0].user ) {
+                                               _data.notifications[title] = 
revisions[0];
+                                       }
+                               }
+                               _data.lastCheck = (new Date()).toISOString();
+                               _persistData();
+                               dfd.resolve( _data.notifications );
+                       });
+
+                       return dfd.promise();
+               }
+       };
+})( mediaWiki, jQuery, document );
\ No newline at end of file
diff --git a/resources/ui/dialogs/th.ui.MessageDialog.js 
b/resources/ui/dialogs/th.ui.MessageDialog.js
new file mode 100644
index 0000000..229cb05
--- /dev/null
+++ b/resources/ui/dialogs/th.ui.MessageDialog.js
@@ -0,0 +1,53 @@
+/*!
+ * Teahouse user interface MessageDialog class.
+ */
+
+/**
+ * Dialog displaying a message to a user
+ *
+ * @class
+ * @extends OO.ui.MessageDialog
+ *
+ * @constructor
+ * @param {Object} [config] Configuration options
+ */
+
+th.ui.MessageDialog = function ThUiMessageDialog( config ) {
+       th.ui.MessageDialog.super.call( this, config );
+};
+OO.inheritClass( th.ui.MessageDialog, OO.ui.MessageDialog );
+
+th.ui.MessageDialog.static.actions = [
+       { action: 'yes', label: mw.message('th-dialog-msg-btn-yes').plain() },
+       { action: 'no', label: mw.message('th-dialog-msg-btn-no').plain() }
+];
+/**
+ * @inheritdoc
+ */
+th.ui.MessageDialog.prototype.initialize = function () {
+       // Parent method
+       th.ui.MessageDialog.super.prototype.initialize.call( this );
+
+       //Create new panel
+       this.$message = $('<div>').addClass( 'oo-ui-messageDialog-message' );
+
+       this.text.$element.append( this.$message );
+};
+
+/**
+ * @inheritdoc
+ */
+th.ui.MessageDialog.prototype.getSetupProcess = function ( data ) {
+       data = data || {};
+
+       // Parent method
+       return th.ui.MessageDialog.super.prototype.getSetupProcess.call( this, 
data )
+               .next( function () {
+
+                       //We hide the base class' OO.ui.LabelWidget
+                       this.message.$element.hide();
+
+                       this.$message.empty();
+                       this.$message.append(data.message);
+               }, this );
+};
\ No newline at end of file
diff --git a/resources/ui/dialogs/th.ui.QuestionDialog.js 
b/resources/ui/dialogs/th.ui.QuestionDialog.js
index e1c355c..73dac5d 100644
--- a/resources/ui/dialogs/th.ui.QuestionDialog.js
+++ b/resources/ui/dialogs/th.ui.QuestionDialog.js
@@ -12,7 +12,8 @@
  * @param {Object} [config] Configuration options
  */
 
-th.ui.QuestionDialog = function ThUiQuestionDialog( config ) {
+th.ui.QuestionDialog = function ThUiQuestionDialog( config, teahousecfg ) {
+       this._config = teahousecfg;
        th.ui.QuestionDialog.super.call( this, config );
 };
 OO.inheritClass( th.ui.QuestionDialog, OO.ui.ProcessDialog );
@@ -27,7 +28,6 @@
                disabled: true
        },
        {
-               //action: 'cancel',
                label: mw.message('th-dialog-btn-cancel').plain(),
                flags: 'safe'
        }
@@ -36,8 +36,6 @@
 th.ui.QuestionDialog.prototype.initialize = function () {
        th.ui.QuestionDialog.super.prototype.initialize.apply( this, arguments 
);
 
-       var config = mw.teahouse.getConfig();
-
        this.content = new OO.ui.PanelLayout( {
                $: this.$,
                padded: true,
@@ -45,7 +43,6 @@
                expanded: false
        } );
 
-       //this.tiQuestion = new OO.ui.SearchWidget();
        this.tiQuestion = new OO.ui.TextInputWidget({
                classes: [ 'th-inputWidget', 'th-tiQuestion' ]
        });
@@ -89,10 +86,10 @@
        this.pnlSimilarQuestions = $('<div>');
 
        var anchor =  mw.html.element( 'a', {
-               href: mw.util.getUrl( config.basePage ),
-               title: config.basePage,
+               href: mw.util.getUrl( this._config.basePage ),
+               title: this._config.basePage,
                target: '_blank'
-       }, config.basePage );
+       }, this._config.basePage );
 
        this.content.$element.append(
                $('<p>').append( mw.message('th-dialog-description-top', 
anchor).text() )
@@ -110,46 +107,21 @@
 
 th.ui.QuestionDialog.prototype.onTiQuestionChange = function( value ) {
        var me = this;
-       var config = mw.teahouse.getConfig();
        var actions = this.getActions();
-       this.pnlSimilarQuestions.empty();
+
        //actions.list[0] is the save/publish action
        //If there is no question entered or it is way too short to be a 
question
        //we disable the "save" action
        if(value !== '' && value.length > 3) {
-               var api = new mw.Api();
-               api.get({
-                       action: 'query',
-                       list: 'search',
-                       srsearch: value,
-                       srlimit: 50,
-                       srnamespace: 4 //NS_PROJECT --> todo: make configurable
-               })
-               .done(function( response, jqXHR ){
-                       var list = 
$('<ul>').addClass('th-similar-questions-list');
-                       var count = 0;
-                       for( var i = 0; i < response.query.search.length && 
count < 5; i++ ) {
-                               var title = response.query.search[i].title;
-                               if( title.indexOf( config.basePage ) !== 0 ) {
-                                       continue;
-                               }
-
-                               var displayTitle = title.replace( 
config.basePage, '' );
-                               var anchor =  mw.html.element( 'a', {
-                                       href: mw.util.getUrl( title ),
-                                       title: title,
-                                       target: '_blank'
-                               }, title );
-                               list.append( '<li>'+anchor+'</li>' );
-                               count++;
-                       }
-                       me.pnlSimilarQuestions.append( list );
+               mw.teahouse.board.getSimilarQuestions( value, 
this.pnlSimilarQuestions, function() {
                        me.getManager().updateWindowSize( me );
                });
 
                actions.list[0].setDisabled(false);
                return;
        }
+
+       this.pnlSimilarQuestions.empty();
        this.getManager().updateWindowSize( this );
        actions.list[0].setDisabled(true);
 };
@@ -173,16 +145,10 @@
 
 th.ui.QuestionDialog.prototype.getActionProcess = function ( action ) {
        var me = this;
-       var config = mw.teahouse.getConfig();
 
        var text = this.tiDesc.getValue() + ''; //Implicit conversion to string.
        //Don't know why it is necessary but otherwise adding a "\n" will break
        //the string
-
-       //We append a signature WikiText fragment by default
-       if( text.indexOf( '--~~~~' ) === false ) {
-               text += "\n--~~~~";
-       }
 
        var question = {
                title: this.tiQuestion.getValue(),
@@ -190,42 +156,36 @@
        };
 
        if ( action === 'save' ) {
-               return new OO.ui.Process( function () {
-                       //TODO: Move out of dialog implementation and into "The 
Board"
-                       var api = new mw.Api();
-                       return api.postWithToken( 'edit', {
-                               action: 'edit',
-                               title: config.basePage + "/" + question.title,
-                               summary: "Added by Teahouse gadget",
-                               text: question.text
-                       })
-                       .fail(function( response, jqXHR ){
-                               console.log(arguments);
-                       })
-                       .done(function( response, jqXHR ){
+               return new OO.ui.Process( function() {
+                       return mw.teahouse.board.publishQuestion( question )
+                               .done(function( editdata ) {
+
                                //TODO: add a parameter whether to reset fields 
or not
                                //This parameter will be passed to 
'getTeardownProcess'
-                               me.close( { action: action } );
-                       })
-                       .then(function( response, jqXHR ){
-                               var msg = mw.message('th-dialog-msg-text-save', 
response.edit.title ).text();
+                               me.close( { action: action } )
+                                       .done(function( data ){
 
-                               mw.teahouse.openMessageDialog({
-                                       title: 
mw.message('th-dialog-msg-title-save').plain(),
-                                       message: msg,
-                                       actions: [
-                                               { label: 
mw.message('th-dialog-msg-btn-yes').plain(), action: 'yes' },
-                                               { label: 
mw.message('th-dialog-msg-btn-no').plain(), action: 'no' }
-                                       ]
-                               }).then( function ( opening ) {
-                                       opening.then( function ( opened ) {
-                                               opened.then( function ( data ) {
-                                                       if( data.action === 
'yes' ) {
-                                                               
window.location.href = mw.util.getUrl( config.basePage );
-                                                       }
+                                               var anchor = 
mw.html.element('a', {
+                                                       href: mw.util.getUrl( 
editdata.questionpage.title ),
+                                                       title: 
editdata.questionpage.title,
+                                                       target: '_blank'
+                                               }, editdata.questionpage.title 
);
+
+                                               var msg = 
mw.message('th-dialog-msg-text-save', anchor ).text();
+
+                                               
mw.teahouse.dialog.openMessageDialog({
+                                                       title: 
mw.message('th-dialog-msg-title-save').plain(),
+                                                       message: msg
+                                               }, function ( opening ) {
+                                                       opening.then( function 
( opened ) {
+                                                               opened.then( 
function ( data ) {
+                                                                       if( 
data.action === 'yes' ) {
+                                                                               
window.location.href = mw.util.getUrl( me._config.basePage );
+                                                                       }
+                                                               });
+                                                       });
                                                });
                                        });
-                               });
                        });
                });
        }

-- 
To view, visit https://gerrit.wikimedia.org/r/166749
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I525195eac98ff7addd6f47546f40d50daf51d707
Gerrit-PatchSet: 13
Gerrit-Project: mediawiki/extensions/Teahouse
Gerrit-Branch: master
Gerrit-Owner: Robert Vogel <[email protected]>
Gerrit-Reviewer: Robert Vogel <[email protected]>
Gerrit-Reviewer: Smuggli <[email protected]>
Gerrit-Reviewer: Tweichart <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to