Skip to content

Commit

Permalink
Rework notifications. Add option to prioritize notifications.
Browse files Browse the repository at this point in the history
  • Loading branch information
klntsky committed Oct 24, 2021
1 parent 1db2515 commit 646e969
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 35 deletions.
1 change: 1 addition & 0 deletions src/Data.purs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ type ValidSettings =
, followNotifications :: Boolean
, notificationsTimeout :: Int
, maxNotificationDuration :: Int
, notificationsFirst :: Boolean
}
27 changes: 22 additions & 5 deletions src/Settings.purs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ type Settings =
, followNotifications :: Boolean
, notificationsTimeout :: String
, maxNotificationDuration :: String
, notificationsFirst :: Boolean
}

data CheckBox
Expand All @@ -77,6 +78,7 @@ data CheckBox
| DomainWithSubdomains Int
| WebsitesOnlyIfNoAudible
| FollowNotifications
| NotificationsFirst

data Button
= RemoveDomain Int
Expand Down Expand Up @@ -108,6 +110,7 @@ initialSettings =
, followNotifications: true
, notificationsTimeout: 10
, maxNotificationDuration: 10
, notificationsFirst: true
}

toRuntimeSettings :: ValidSettings -> Settings
Expand Down Expand Up @@ -185,23 +188,33 @@ renderGeneralSettings
]

renderNotifications :: forall m o. State -> H.ComponentHTML Action o m
renderNotifications { validationResult, settings: { followNotifications, notificationsTimeout, maxNotificationDuration } } = div_
renderNotifications { validationResult, settings } = div_
[ h3_ [ text "NOTIFICATIONS" ]
, input [ type_ InputCheckbox
, checked followNotifications
, checked settings.followNotifications
, onChecked $ Toggle FollowNotifications
, id "notifications"
]
, label
[ for "notifications" ]
[ text "Follow notifications" ]
, tooltip $ "Some websites play short notification sounds when user's attention is needed. This option allows to react to a notification during some fixed period of time after the notification sound has ended. A sound is treated as a notification if it is not coming from currently active tab AND its duration is less than " <> maxNotificationDuration <> " seconds. Tabs with notifications will always be shown first, before ordinary audible tabs. However, when the notification sound is still playing, the usual ordering (left-to-right or right-to-left) will apply (because it's impossible to know if a sound is a notification or not, before we know its duration)."
, tooltip $ "Some websites play short notification sounds when user's attention is needed. This option allows to react to a notification during some fixed period of time after the notification sound has ended. A sound is treated as a notification if it is not coming from currently active tab AND its duration is less than notification duration limit (currently set to " <> settings.maxNotificationDuration <> " seconds)."
, br_
, input [ type_ InputCheckbox
, checked settings.notificationsFirst
, onChecked $ Toggle NotificationsFirst
, id "notifications-first"
]
, label
[ for "notifications-first" ]
[ text "Prioritize notifications" ]
, tooltip $ "When checked, tabs with notifications will always be shown first, before ordinary audible tabs."
, br_
, br_
, text "Timeout: "
, input $
[ type_ InputNumber
, value notificationsTimeout
, value settings.notificationsTimeout
, HE.onValueInput $ TextInput <<< TimeoutField
, id "timeout-field"
] <>
Expand All @@ -217,7 +230,7 @@ renderNotifications { validationResult, settings: { followNotifications, notific
, text "Notification duration limit: "
, input $
[ type_ InputNumber
, value maxNotificationDuration
, value settings.maxNotificationDuration
, HE.onValueInput $ TextInput <<< DurationField
, id "duration-field"
] <>
Expand Down Expand Up @@ -394,6 +407,8 @@ handleAction (Toggle checkbox value) = do
_markAsAudible <<< ix index %~ set _withSubdomains value
FollowNotifications ->
_followNotifications .~ value
NotificationsFirst ->
_notificationsFirst .~ value
saveSettings

saveSettings :: forall a i o. H.HalogenM State a i o Aff Unit
Expand Down Expand Up @@ -436,6 +451,7 @@ validate settings =
, followNotifications: settings.followNotifications
, notificationsTimeout: timeout
, maxNotificationDuration: duration
, notificationsFirst: settings.notificationsFirst
}
_ ->
Left
Expand All @@ -462,3 +478,4 @@ _validationResult = prop (SProxy :: SProxy "validationResult")
_notificationsTimeout = prop (SProxy :: SProxy "notificationsTimeout")
_followNotifications = prop (SProxy :: SProxy "followNotifications")
_maxNotificationDuration = prop (SProxy :: SProxy "maxNotificationDuration")
_notificationsFirst = prop (SProxy :: SProxy "notificationsFirst")
81 changes: 51 additions & 30 deletions src/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const defaults = {
websitesOnlyIfNoAudible: false,
followNotifications: true,
notificationsTimeout: 10,
maxNotificationDuration: 10
maxNotificationDuration: 10,
notificationsFirst: true,
};

// A flag indicating that no tabs are selected by queries.
Expand All @@ -34,7 +35,14 @@ const MENU_ID = "mark-as-audible";

// Used to follow notifications
const currentlyAudible = new Map(); // tabId => timestamp
let notifications = []; // [{ tabId : Int, tabIndex: Int }]

const catcher = (f) => async function () {
try {
return await f(...arguments);
} catch (e) {
console.log('Error in', unescape(f), e);
}
};

const addMarkedTab = tab => {
if (!marked.some(mkd => mkd.id === tab.id)) {
Expand Down Expand Up @@ -64,7 +72,8 @@ const runSettingsMigrations = settings => {
'websitesOnlyIfNoAudible',
'followNotifications',
'notificationsTimeout',
'maxNotificationDuration'
'maxNotificationDuration',
'notificationsFirst'
];

for (let prop of added_props) {
Expand All @@ -77,9 +86,10 @@ const runSettingsMigrations = settings => {
};

/** Returns settings object */
const loadSettings = () => browser.storage.local.get({
settings: defaults
}).then(r => {
const loadSettings = catcher(async () => {
const r = await browser.storage.local.get({
settings: defaults
});

// Set global variable
settings = runSettingsMigrations(r.settings) ;
Expand Down Expand Up @@ -173,7 +183,7 @@ browser.tabs.onRemoved.addListener(tabId => {
firstActive = null;
}
marked = marked.filter(mkd => mkd.id !== tabId);
notifications = notifications.filter(tb => tb.id !== tabId);
currentlyAudible.delete(tabId);
});

// Track the last active tab which was activated by the user or another
Expand All @@ -195,16 +205,16 @@ browser.tabs.onActivated.addListener(async ({ tabId, windowId }) => {
}
});

browser.windows.onFocusChanged.addListener(async (windowId) => {
browser.windows.onFocusChanged.addListener(catcher(async (windowId) => {
const activeTab = await getActiveTab();
const checked = marked.some(mkd => mkd.id === activeTab.id);
updateIcon(checked);
if (lastTabs.every(tab => tab.id !== activeTab.id)) {
firstActive = activeTab;
}
});
}));

browser.browserAction.onClicked.addListener(async () => {
browser.browserAction.onClicked.addListener(catcher(async () => {
// Choose how to switch to the tab, depending on `settings.allWindows`.
// Maintain waitingForActivation flag.
const switchTo = async (tab, activeTab) => {
Expand Down Expand Up @@ -265,11 +275,28 @@ browser.browserAction.onClicked.addListener(async () => {
tabs = [...tabs, ...await query(refine({ url: permanentlyMarked }))];
}

tabs = sortTabs(tabs);

// More recent notifications should always be first.
if (settings.followNotifications) {
tabs = [...notifications, ...tabs];

// Extract notifications from currentlyAudible
const now = Date.now();
let notifications = [...currentlyAudible.values()].filter(([start, end, tab]) => {
end = end || now;
return end - start < settings.maxNotificationDuration * 1000;
});

// Sort by starting time. Newest first.
notifications.sort((a, b) => b[0] - a[0]);
notifications = notifications.map(([_start, _end, tab]) => tab);

if (settings.notificationsFirst) {
// Prepend before others
tabs = [...notifications, ...sortTabs(tabs)];
} else {
// Sort everything
tabs = sortTabs([...notifications, ...tabs]);
}
} else {
tabs = sortTabs(tabs);
}

tabs = filterRepeating(tabs);
Expand Down Expand Up @@ -302,7 +329,7 @@ browser.browserAction.onClicked.addListener(async () => {
default:
await switchTo(next, activeTab);
}
});
}));

browser.menus.onShown.addListener(async function(info, tab) {
if (info.menuIds.includes(MENU_ID)) {
Expand Down Expand Up @@ -339,24 +366,18 @@ browser.menus.onClicked.addListener(async function(info, tab) {
browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
if (typeof changeInfo.audible == 'boolean') {
if (changeInfo.audible) {
currentlyAudible.set(tabId, Date.now());
currentlyAudible.set(tabId, [Date.now(), null, tab]);
} else {
if (currentlyAudible.has(tabId)) {
const startTime = currentlyAudible.get(tabId);
const duration = Date.now() - startTime;
currentlyAudible.delete(tabId);
// the sound is short enough to be considered a notification
if (duration < settings.maxNotificationDuration * 1000) {
// We are not trying to add a tab we are currently on.
if ((await getActiveTab()).id != tab.id) {
// Add it to notifications
notifications.unshift(tab);
// And schedule a deletion
setTimeout(() => {
notifications = notifications.filter(tb => tb.id != tabId);
}, settings.notificationsTimeout * 1000);
const [startTime, _end, _tab] = currentlyAudible.get(tabId);
const now = Date.now();
currentlyAudible.set(tabId, [startTime, now, tab]);
setTimeout(() => {
// Delete only if we added it.
if (currentlyAudible.get(tabId)[0] == startTime) {
currentlyAudible.delete(tabId);
}
}
}, settings.notificationsTimeout * 1000);
}
}
}
Expand Down

0 comments on commit 646e969

Please sign in to comment.