about summary refs log tree commit diff
path: root/app/javascript/flavours/glitch/service_worker/web_push_notifications.js
blob: f63cff335a16c970c38fa9b29a72dbb7237e91d2 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
const MAX_NOTIFICATIONS = 5;
const GROUP_TAG = 'tag';

// Avoid loading intl-messageformat and dealing with locales in the ServiceWorker
const formatGroupTitle = (message, count) => message.replace('%{count}', count);

const notify = options =>
  self.registration.getNotifications().then(notifications => {
    if (notifications.length === MAX_NOTIFICATIONS) {
      // Reached the maximum number of notifications, proceed with grouping
      const group = {
        title: formatGroupTitle(options.data.message, notifications.length + 1),
        body: notifications
          .sort((n1, n2) => n1.timestamp < n2.timestamp)
          .map(notification => notification.title).join('\n'),
        badge: '/badge.png',
        icon: '/android-chrome-192x192.png',
        tag: GROUP_TAG,
        data: {
          url: (new URL('/web/notifications', self.location)).href,
          count: notifications.length + 1,
          message: options.data.message,
        },
      };

      notifications.forEach(notification => notification.close());

      return self.registration.showNotification(group.title, group);
    } else if (notifications.length === 1 && notifications[0].tag === GROUP_TAG) {
      // Already grouped, proceed with appending the notification to the group
      const group = cloneNotification(notifications[0]);

      group.title = formatGroupTitle(group.data.message, group.data.count + 1);
      group.body  = `${options.title}\n${group.body}`;
      group.data  = { ...group.data, count: group.data.count + 1 };

      return self.registration.showNotification(group.title, group);
    }

    return self.registration.showNotification(options.title, options);
  });

const handlePush = (event) => {
  const options = event.data.json();

  options.body      = options.data.nsfw || options.data.content;
  options.dir       = options.data.dir;
  options.image     = options.image || undefined; // Null results in a network request (404)
  options.timestamp = options.timestamp && new Date(options.timestamp);

  const expandAction = options.data.actions.find(action => action.todo === 'expand');

  if (expandAction) {
    options.actions          = [expandAction];
    options.hiddenActions    = options.data.actions.filter(action => action !== expandAction);
    options.data.hiddenImage = options.image;
    options.image            = undefined;
  } else {
    options.actions = options.data.actions;
  }

  event.waitUntil(notify(options));
};

const cloneNotification = (notification) => {
  const clone = {  };

  for(var k in notification) {
    clone[k] = notification[k];
  }

  return clone;
};

const expandNotification = (notification) => {
  const nextNotification = cloneNotification(notification);

  nextNotification.body    = notification.data.content;
  nextNotification.image   = notification.data.hiddenImage;
  nextNotification.actions = notification.data.actions.filter(action => action.todo !== 'expand');

  return self.registration.showNotification(nextNotification.title, nextNotification);
};

const makeRequest = (notification, action) =>
  fetch(action.action, {
    headers: {
      'Authorization': `Bearer ${notification.data.access_token}`,
      'Content-Type': 'application/json',
    },
    method: action.method,
    credentials: 'include',
  });

const findBestClient = clients => {
  const focusedClient = clients.find(client => client.focused);
  const visibleClient = clients.find(client => client.visibilityState === 'visible');

  return focusedClient || visibleClient || clients[0];
};

const openUrl = url =>
  self.clients.matchAll({ type: 'window' }).then(clientList => {
    if (clientList.length !== 0) {
      const webClients = clientList.filter(client => /\/web\//.test(client.url));

      if (webClients.length !== 0) {
        const client       = findBestClient(webClients);
        const { pathname } = new URL(url);

        if (pathname.startsWith('/web/')) {
          return client.focus().then(client => client.postMessage({
            type: 'navigate',
            path: pathname.slice('/web/'.length - 1),
          }));
        }
      } else if ('navigate' in clientList[0]) { // Chrome 42-48 does not support navigate
        const client = findBestClient(clientList);

        return client.navigate(url).then(client => client.focus());
      }
    }

    return self.clients.openWindow(url);
  });

const removeActionFromNotification = (notification, action) => {
  const actions          = notification.actions.filter(act => act.action !== action.action);
  const nextNotification = cloneNotification(notification);

  nextNotification.actions = actions;

  return self.registration.showNotification(nextNotification.title, nextNotification);
};

const handleNotificationClick = (event) => {
  const reactToNotificationClick = new Promise((resolve, reject) => {
    if (event.action) {
      const action = event.notification.data.actions.find(({ action }) => action === event.action);

      if (action.todo === 'expand') {
        resolve(expandNotification(event.notification));
      } else if (action.todo === 'request') {
        resolve(makeRequest(event.notification, action)
          .then(() => removeActionFromNotification(event.notification, action)));
      } else {
        reject(`Unknown action: ${action.todo}`);
      }
    } else {
      event.notification.close();
      resolve(openUrl(event.notification.data.url));
    }
  });

  event.waitUntil(reactToNotificationClick);
};

self.addEventListener('push', handlePush);
self.addEventListener('notificationclick', handleNotificationClick);