[gnome-shell/T29763: 174/249] payg: factor out reminder notifications



commit c4830ceaf2df04ad4eb3ca7c427dacfc5afbe795
Author: Travis Reitter <travis reitter endlessm com>
Date:   Mon Dec 3 22:51:57 2018 -0800

    payg: factor out reminder notifications
    
    This will allow re-use by the upcoming PayGo status applet
    
    https://phabricator.endlessm.com/T24125

 js/misc/paygManager.js |  90 +----------------
 js/ui/payg.js          | 255 ++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 258 insertions(+), 87 deletions(-)
---
diff --git a/js/misc/paygManager.js b/js/misc/paygManager.js
index 49d236c247..d252bf0e5a 100644
--- a/js/misc/paygManager.js
+++ b/js/misc/paygManager.js
@@ -27,9 +27,7 @@ const { Gio, GLib, GnomeDesktop, GObject, Shell } = imports.gi;
 
 const { loadInterfaceXML } = imports.misc.fileUtils;
 
-const Gettext = imports.gettext;
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
+const Payg = imports.ui.payg;
 
 const EOS_PAYG_NAME = 'com.endlessm.Payg1';
 const EOS_PAYG_PATH = '/com/endlessm/Payg1';
@@ -52,10 +50,6 @@ const DBusErrorsMapping = {
     DISABLED: 'com.endlessm.Payg1.Error.Disabled',
 };
 
-// Title and description text to be shown in the periodic reminders.
-const NOTIFICATION_TITLE_TEXT = _('Pay as You Go');
-const NOTIFICATION_DETAILED_FORMAT_STRING = _('Subscription runs out in %s.');
-
 // This list defines the different instants in time where we would
 // want to show notifications to the user reminding that the payg
 // subscription will be expiring soon, up to a max GLib.MAXUINT32.
@@ -73,57 +67,6 @@ const notificationAlertTimesSecs = [
     30,           // 30 seconds
 ];
 
-// Takes an UNIX timestamp (in seconds) and returns a string
-// with a precision level appropriate to show to the user.
-//
-// The returned string will be formatted just in seconds for times
-// under 1 minute, in minutes for times under 2 hours, in hours and
-// minutes (if applicable) for times under 1 day, and then in days
-// and hours (if applicable) for anything longer than that in days.
-//
-// Some examples:
-//   - 45 seconds => "45 seconds"
-//   - 60 seconds => "1 minute"
-//   - 95 seconds => "1 minute"
-//   - 120 seconds => "2 minutes"
-//   - 3600 seconds => "60 minutes"
-//   - 4500 seconds => "75 minutes"
-//   - 7200 seconds => "2 hours"
-//   - 8640 seconds => "2 hours 24 minutes"
-//   - 86400 seconds => "1 day"
-//   - 115200 seconds => "1 day 8 hours"
-//   - 172800 seconds => "2 days"
-function timeToString(seconds) {
-    if (seconds < 60)
-        return Gettext.ngettext('%s second', '%s seconds', seconds).format(Math.floor(seconds));
-
-    let minutes = Math.floor(seconds / 60);
-    if (minutes < 120)
-        return Gettext.ngettext('%s minute', '%s minutes', minutes).format(minutes);
-
-    let hours = Math.floor(minutes / 60);
-    if (hours < 24) {
-        let hoursStr = Gettext.ngettext('%s hour', '%s hours', hours).format(hours);
-
-        let minutesPast = minutes % 60;
-        if (minutesPast == 0)
-            return hoursStr;
-
-        let minutesStr = Gettext.ngettext('%s minute', '%s minutes', minutesPast).format(minutesPast);
-        return ("%s %s").format(hoursStr, minutesStr);
-    }
-
-    let days = Math.floor(hours / 24);
-    let daysStr = Gettext.ngettext('%s day', '%s days', days).format(days);
-
-    let hoursPast = hours % 24;
-    if (hoursPast == 0)
-        return daysStr;
-
-    let hoursStr = Gettext.ngettext('%s hour', '%s hours', hoursPast).format(hoursPast);
-    return ("%s %s").format(daysStr, hoursStr);
-}
-
 var PaygManager = GObject.registerClass({
     Signals: {
         'code-expired': {},
@@ -145,7 +88,7 @@ var PaygManager = GObject.registerClass({
         this._rateLimitEndTime = 0;
         this._codeFormat = '';
         this._codeFormatRegex = null;
-        this._notification = null;
+        this._paygNotifier = new Payg.PaygNotifier();
 
         // Keep track of clock changes to update notifications.
         this._wallClock = new GnomeDesktop.WallClock({ time_only: true });
@@ -269,31 +212,6 @@ var PaygManager = GObject.registerClass({
         this._updateExpirationReminders();
     }
 
-    _notifyPaygReminder(secondsLeft) {
-        // Only notify when in an regular session, not in GDM or initial-setup.
-        if (Main.sessionMode.currentMode != 'user' &&
-            Main.sessionMode.currentMode != 'endless' &&
-            Main.sessionMode.currentMode != 'user-coding')
-            return;
-
-        if (this._notification)
-            this._notification.destroy();
-
-        let source = new MessageTray.SystemNotificationSource();
-        Main.messageTray.add(source);
-
-        let timeLeft = timeToString(secondsLeft);
-        this._notification = new MessageTray.Notification(
-            source,
-            NOTIFICATION_TITLE_TEXT,
-            NOTIFICATION_DETAILED_FORMAT_STRING.format(timeLeft));
-        this._notification.setUrgency(MessageTray.Urgency.HIGH);
-        this._notification.setTransient(false);
-        source.notify(this._notification);
-
-        this._notification.connect('destroy', () => this._notification = null);
-    }
-
     _maybeNotifyUser() {
         // Sanity check.
         if (notificationAlertTimesSecs.length === 0)
@@ -301,7 +219,7 @@ var PaygManager = GObject.registerClass({
 
         let secondsLeft = this.timeRemainingSecs();
         if (secondsLeft > 0 && secondsLeft <= notificationAlertTimesSecs[0])
-            this._notifyPaygReminder(secondsLeft);
+            this._paygNotifier.notify(secondsLeft);
     }
 
     _updateExpirationReminders() {
@@ -336,7 +254,7 @@ var PaygManager = GObject.registerClass({
             () => {
                 // We want to show "round" numbers in the notification, matching
                 // whatever is specified in the notificationAlertTimeSecs array.
-                this._notifyPaygReminder(targetAlertTime);
+                this._paygNotifier.notify(targetAlertTime);
 
                 // Reset _expirationReminderId before _updateExpirationReminders()
                 // to prevent an attempt to remove the same GSourceFunc twice.
diff --git a/js/ui/payg.js b/js/ui/payg.js
index 04b1efb2cf..cb2d59b54b 100644
--- a/js/ui/payg.js
+++ b/js/ui/payg.js
@@ -18,14 +18,17 @@
 // along with this program; if not, write to the Free Software
 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
-/* exported PaygUnlockUi, SPINNER_ICON_SIZE_PIXELS, SUCCESS_DELAY_SECONDS */
+/* exported PaygUnlockUi,  SPINNER_ICON_SIZE_PIXELS, SUCCESS_DELAY_SECONDS,
+    ApplyCodeNotification, timeToString */
 
 const { Clutter, Gio, GLib, GObject, Shell, St } = imports.gi;
 
 const PaygManager = imports.misc.paygManager;
 
 const Gettext = imports.gettext;
+const Animation = imports.ui.animation;
 const Main = imports.ui.main;
+const MessageTray = imports.ui.messageTray;
 
 var SUCCESS_DELAY_SECONDS = 3;
 
@@ -33,6 +36,10 @@ var SPINNER_ICON_SIZE_PIXELS = 16;
 var SPINNER_ANIMATION_DELAY_MSECS = 1000;
 var SPINNER_ANIMATION_TIME_MSECS = 300;
 
+const NOTIFICATION_TITLE_TEXT = _('Pay as You Go');
+const NOTIFICATION_EARLY_CODE_ENTRY_TEXT = _('Enter an unlock code to extend PayGo time before expiration.');
+const NOTIFICATION_DETAILED_FORMAT_STRING = _('Subscription runs out in %s.');
+
 var UnlockStatus = {
     NOT_VERIFYING: 0,
     VERIFYING: 1,
@@ -212,3 +219,249 @@ class PaygUnlockUi extends St.Widget {
         });
     }
 });
+
+var PaygUnlockWidget = GObject.registerClass({
+    Signals: {
+        'code-added': {},
+        'code-rejected': { param_types: [GObject.TYPE_STRING] },
+    },
+}, class PaygUnlockWidget extends PaygUnlockUi {
+
+    _init() {
+        super._init();
+
+        this._verificationStatus = UnlockStatus.NOT_VERIFYING;
+        this._codeEntry = this._createCodeEntry();
+        this._spinner = this._createSpinner();
+        let entrySpinnerBox = new St.BoxLayout({
+            style_class: 'notification-actions',
+            x_expand: false,
+        });
+        entrySpinnerBox.add_child(this._codeEntry);
+        entrySpinnerBox.add_child(this._spinner.actor);
+
+        this._buttonBox = new St.BoxLayout({
+            style_class: 'notification-actions',
+            x_expand: true,
+            vertical: true,
+        });
+        global.focus_manager.add_group(this._buttonBox);
+        this._buttonBox.add_child(entrySpinnerBox);
+
+        this._applyButton = this._createApplyButton();
+        this._applyButton.connect('clicked', this.startVerifyingCode.bind(this));
+        this._buttonBox.add_child(this._applyButton);
+
+        this.updateSensitivity();
+    }
+
+    _createCodeEntry() {
+        let codeEntry = new St.Entry({
+            style_class: 'notification-payg-entry',
+            x_expand: true,
+            can_focus: true,
+        });
+        codeEntry.clutter_text.connect('activate', this.startVerifyingCode.bind(this));
+        codeEntry.clutter_text.connect('text-changed', this.updateApplyButtonSensitivity.bind(this));
+        codeEntry._enabled = true;
+
+        return codeEntry;
+    }
+
+    _createSpinner() {
+        // We make the most of the spacer to show the spinner while verifying the code.
+        let spinnerIcon = Gio.File.new_for_uri('resource:///org/gnome/shell/theme/process-working.svg');
+        let spinner = new Animation.AnimatedIcon(spinnerIcon, SPINNER_ICON_SIZE_PIXELS);
+        spinner.actor.opacity = 0;
+        spinner.actor.hide();
+
+        return spinner;
+    }
+
+    _createApplyButton() {
+        let box = new St.BoxLayout();
+
+        let label = new St.Bin({
+            x_expand: true,
+            child: new St.Label({
+                x_expand: true,
+                x_align: Clutter.ActorAlign.CENTER,
+                text: _('Apply Code'),
+            }),
+        });
+        box.add_child(label);
+
+        let button = new St.Button({
+            child: box,
+            x_fill: true,
+            x_expand: true,
+            button_mask: St.ButtonMask.ONE,
+            style_class: 'hotplug-notification-item button',
+        });
+
+        return button;
+    }
+
+    setErrorMessage(message) {
+        this.emit('code-rejected', message);
+    }
+
+    _onEntryChanged() {
+        this.updateApplyButtonSensitivity();
+    }
+
+    onCodeAdded() {
+        this.emit('code-added');
+    }
+
+    entryReset() {
+        this._codeEntry.set_text('');
+    }
+
+    entrySetEnabled(enabled) {
+        if (this._codeEntry._enabled === enabled)
+            return;
+
+        this._codeEntry._enabled = enabled;
+        this._codeEntry.reactive = enabled;
+        this._codeEntry.can_focus = enabled;
+        this._codeEntry.clutter_text.reactive = enabled;
+        this._codeEntry.clutter_text.editable = enabled;
+        this._codeEntry.clutter_text.cursor_visible = enabled;
+    }
+
+    get entryCode() {
+        return this._codeEntry.get_text();
+    }
+
+    get verificationStatus() {
+        return this._verificationStatus;
+    }
+
+    set verificationStatus(value) {
+        this._verificationStatus = value;
+    }
+
+    get spinner() {
+        return this._spinner;
+    }
+
+    get applyButton() {
+        return this._applyButton;
+    }
+
+    get buttonBox() {
+        return this._buttonBox;
+    }
+
+});
+
+var ApplyCodeNotification = GObject.registerClass(
+class ApplyCodeNotification extends MessageTray.Notification {
+    _init(source, title, banner) {
+        super._init(source, title, banner);
+
+        this._titleOrig = title;
+
+        // Note: "banner" is actually the string displayed in the banner, not a
+        // banner object. This variable name simply follows the convention of
+        // the parent class.
+        this._bannerOrig = banner;
+        this._verificationStatus = UnlockStatus.NOT_VERIFYING;
+    }
+
+    createBanner() {
+        this._banner = new MessageTray.NotificationBanner(this);
+        this._unlockWidget = new PaygUnlockWidget();
+        this._unlockWidget.connect('code-added', this._onCodeAdded.bind(this));
+        this._unlockWidget.connect('code-rejected', this._onCodeRejected.bind(this));
+        this._banner.setActionArea(this._unlockWidget.buttonBox);
+
+        return this._banner;
+    }
+
+    _onCodeAdded() {
+        this._setMessage(_('Code applied successfully!'));
+
+        GLib.timeout_add_seconds(
+            GLib.PRIORITY_DEFAULT,
+            SUCCESS_DELAY_SECONDS,
+            () => {
+                this.emit('done-displaying');
+                this.destroy();
+
+                return GLib.SOURCE_REMOVE;
+            });
+    }
+
+    // if errorMessage is unspecified, a default message will be populated based
+    // on whether time remains
+    _onCodeRejected(unlockWidget, errorMessage) {
+        this._setMessage(errorMessage ? errorMessage : this._bannerOrig);
+    }
+
+    _setMessage(message) {
+        this.update(this._titleOrig, message);
+    }
+
+    activate() {
+        // We get here if the Apply button is inactive when we try to click it.
+        // Unless we're already done, exit early so we don't destroy the
+        // notification)
+        if (this._verificationStatus !== UnlockStatus.SUCCEEDED)
+            return;
+
+        super.activate();
+    }
+});
+
+// Takes an UNIX timestamp (in seconds) and returns a string
+// with a precision level appropriate to show to the user.
+//
+// The returned string will be formatted just in seconds for times
+// under 1 minute, in minutes for times under 2 hours, in hours and
+// minutes (if applicable) for times under 1 day, and then in days
+// and hours (if applicable) for anything longer than that in days.
+//
+// Some examples:
+//   - 45 seconds => "45 seconds"
+//   - 60 seconds => "1 minute"
+//   - 95 seconds => "1 minute"
+//   - 120 seconds => "2 minutes"
+//   - 3600 seconds => "60 minutes"
+//   - 4500 seconds => "75 minutes"
+//   - 7200 seconds => "2 hours"
+//   - 8640 seconds => "2 hours 24 minutes"
+//   - 86400 seconds => "1 day"
+//   - 115200 seconds => "1 day 8 hours"
+//   - 172800 seconds => "2 days"
+function timeToString(seconds) {
+    if (seconds < 60)
+        return Gettext.ngettext('%s second', '%s seconds', seconds).format(Math.floor(seconds));
+
+    let minutes = Math.floor(seconds / 60);
+    if (minutes < 120)
+        return Gettext.ngettext('%s minute', '%s minutes', minutes).format(minutes);
+
+    let hours = Math.floor(minutes / 60);
+    if (hours < 24) {
+        let hoursStr = Gettext.ngettext('%s hour', '%s hours', hours).format(hours);
+
+        let minutesPast = minutes % 60;
+        if (minutesPast === 0)
+            return hoursStr;
+
+        let minutesStr = Gettext.ngettext('%s minute', '%s minutes', minutesPast).format(minutesPast);
+        return '%s %s'.format(hoursStr, minutesStr);
+    }
+
+    let days = Math.floor(hours / 24);
+    let daysStr = Gettext.ngettext('%s day', '%s days', days).format(days);
+
+    let hoursPast = hours % 24;
+    if (hoursPast === 0)
+        return daysStr;
+
+    let hoursStr = Gettext.ngettext('%s hour', '%s hours', hoursPast).format(hoursPast);
+    return '%s %s'.format(daysStr, hoursStr);
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]