diff options
Diffstat (limited to 'app/javascript')
-rw-r--r-- | app/javascript/core/admin.js | 53 | ||||
-rw-r--r-- | app/javascript/flavours/glitch/util/main.js | 26 | ||||
-rw-r--r-- | app/javascript/mastodon/components/timeline_hint.js | 2 | ||||
-rw-r--r-- | app/javascript/mastodon/main.js | 26 | ||||
-rw-r--r-- | app/javascript/mastodon/service_worker/entry.js | 81 | ||||
-rw-r--r-- | app/javascript/mastodon/service_worker/web_push_notifications.js | 7 | ||||
-rw-r--r-- | app/javascript/mastodon/storage/db.js | 27 | ||||
-rw-r--r-- | app/javascript/mastodon/storage/modifier.js | 211 | ||||
-rw-r--r-- | app/javascript/styles/mastodon/tables.scss | 49 |
9 files changed, 192 insertions, 290 deletions
diff --git a/app/javascript/core/admin.js b/app/javascript/core/admin.js index c1b9f07a4..c84f566a3 100644 --- a/app/javascript/core/admin.js +++ b/app/javascript/core/admin.js @@ -6,18 +6,71 @@ import ready from '../mastodon/ready'; const batchCheckboxClassName = '.batch-checkbox input[type="checkbox"]'; +const showSelectAll = () => { + const selectAllMatchingElement = document.querySelector('.batch-table__select-all'); + selectAllMatchingElement.classList.add('active'); +}; + +const hideSelectAll = () => { + const selectAllMatchingElement = document.querySelector('.batch-table__select-all'); + const hiddenField = document.querySelector('#select_all_matching'); + const selectedMsg = document.querySelector('.batch-table__select-all .selected'); + const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected'); + + selectAllMatchingElement.classList.remove('active'); + selectedMsg.classList.remove('active'); + notSelectedMsg.classList.add('active'); + hiddenField.value = '0'; +}; + delegate(document, '#batch_checkbox_all', 'change', ({ target }) => { + const selectAllMatchingElement = document.querySelector('.batch-table__select-all'); + [].forEach.call(document.querySelectorAll(batchCheckboxClassName), (content) => { content.checked = target.checked; }); + + if (selectAllMatchingElement) { + if (target.checked) { + showSelectAll(); + } else { + hideSelectAll(); + } + } +}); + +delegate(document, '.batch-table__select-all button', 'click', () => { + const hiddenField = document.querySelector('#select_all_matching'); + const active = hiddenField.value === '1'; + const selectedMsg = document.querySelector('.batch-table__select-all .selected'); + const notSelectedMsg = document.querySelector('.batch-table__select-all .not-selected'); + + if (active) { + hiddenField.value = '0'; + selectedMsg.classList.remove('active'); + notSelectedMsg.classList.add('active'); + } else { + hiddenField.value = '1'; + notSelectedMsg.classList.remove('active'); + selectedMsg.classList.add('active'); + } }); delegate(document, batchCheckboxClassName, 'change', () => { const checkAllElement = document.querySelector('#batch_checkbox_all'); + const selectAllMatchingElement = document.querySelector('.batch-table__select-all'); if (checkAllElement) { checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked); checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked); + + if (selectAllMatchingElement) { + if (checkAllElement.checked) { + showSelectAll(); + } else { + hideSelectAll(); + } + } } }); diff --git a/app/javascript/flavours/glitch/util/main.js b/app/javascript/flavours/glitch/util/main.js index 6577b70c2..9e76774b6 100644 --- a/app/javascript/flavours/glitch/util/main.js +++ b/app/javascript/flavours/glitch/util/main.js @@ -1,9 +1,9 @@ -import * as registerPushNotifications from 'flavours/glitch/actions/push_notifications'; -import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications'; -import { default as Mastodon, store } from 'flavours/glitch/containers/mastodon'; import React from 'react'; import ReactDOM from 'react-dom'; -import ready from './ready'; +import * as registerPushNotifications from 'flavours/glitch/actions/push_notifications'; +import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications'; +import Mastodon, { store } from 'flavours/glitch/containers/mastodon'; +import ready from 'flavours/glitch/util/ready'; const perf = require('./performance'); @@ -24,10 +24,20 @@ function main() { ReactDOM.render(<Mastodon {...props} />, mountNode); store.dispatch(setupBrowserNotifications()); - if (process.env.NODE_ENV === 'production') { - // avoid offline in dev mode because it's harder to debug - require('offline-plugin/runtime').install(); - store.dispatch(registerPushNotifications.register()); + + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + import('workbox-window') + .then(({ Workbox }) => { + const wb = new Workbox('/sw.js'); + + return wb.register(); + }) + .then(() => { + store.dispatch(registerPushNotifications.register()); + }) + .catch(err => { + console.error(err); + }); } perf.stop('main()'); }); diff --git a/app/javascript/mastodon/components/timeline_hint.js b/app/javascript/mastodon/components/timeline_hint.js index fb55a62cc..ac9a79dcc 100644 --- a/app/javascript/mastodon/components/timeline_hint.js +++ b/app/javascript/mastodon/components/timeline_hint.js @@ -6,7 +6,7 @@ const TimelineHint = ({ resource, url }) => ( <div className='timeline-hint'> <strong><FormattedMessage id='timeline_hint.remote_resource_not_displayed' defaultMessage='{resource} from other servers are not displayed.' values={{ resource }} /></strong> <br /> - <a href={url} target='_blank'><FormattedMessage id='account.browse_more_on_origin_server' defaultMessage='Browse more on the original profile' /></a> + <a href={url} target='_blank' rel='noopener'><FormattedMessage id='account.browse_more_on_origin_server' defaultMessage='Browse more on the original profile' /></a> </div> ); diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js index bda51f692..a66975bfd 100644 --- a/app/javascript/mastodon/main.js +++ b/app/javascript/mastodon/main.js @@ -1,9 +1,9 @@ -import * as registerPushNotifications from './actions/push_notifications'; -import { setupBrowserNotifications } from './actions/notifications'; -import { default as Mastodon, store } from './containers/mastodon'; import React from 'react'; import ReactDOM from 'react-dom'; -import ready from './ready'; +import * as registerPushNotifications from 'mastodon/actions/push_notifications'; +import { setupBrowserNotifications } from 'mastodon/actions/notifications'; +import Mastodon, { store } from 'mastodon/containers/mastodon'; +import ready from 'mastodon/ready'; const perf = require('./performance'); @@ -24,10 +24,20 @@ function main() { ReactDOM.render(<Mastodon {...props} />, mountNode); store.dispatch(setupBrowserNotifications()); - if (process.env.NODE_ENV === 'production') { - // avoid offline in dev mode because it's harder to debug - require('offline-plugin/runtime').install(); - store.dispatch(registerPushNotifications.register()); + + if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { + import('workbox-window') + .then(({ Workbox }) => { + const wb = new Workbox('/sw.js'); + + return wb.register(); + }) + .then(() => { + store.dispatch(registerPushNotifications.register()); + }) + .catch(err => { + console.error(err); + }); } perf.stop('main()'); }); diff --git a/app/javascript/mastodon/service_worker/entry.js b/app/javascript/mastodon/service_worker/entry.js index b354f3b33..e4c66cc00 100644 --- a/app/javascript/mastodon/service_worker/entry.js +++ b/app/javascript/mastodon/service_worker/entry.js @@ -1,20 +1,59 @@ -// import { freeStorage, storageFreeable } from '../storage/modifier'; -import './web_push_notifications'; +import { ExpirationPlugin } from 'workbox-expiration'; +import { precacheAndRoute } from 'workbox-precaching'; +import { registerRoute } from 'workbox-routing'; +import { CacheFirst } from 'workbox-strategies'; +import { handleNotificationClick, handlePush } from './web_push_notifications'; -// function openSystemCache() { -// return caches.open('mastodon-system'); -// } +const CACHE_NAME_PREFIX = 'mastodon-'; function openWebCache() { - return caches.open('mastodon-web'); + return caches.open(`${CACHE_NAME_PREFIX}web`); } function fetchRoot() { return fetch('/', { credentials: 'include', redirect: 'manual' }); } -// const firefox = navigator.userAgent.match(/Firefox\/(\d+)/); -// const invalidOnlyIfCached = firefox && firefox[1] < 60; +precacheAndRoute(self.__WB_MANIFEST); + +registerRoute( + /locale_.*\.js$/, + new CacheFirst({ + cacheName: `${CACHE_NAME_PREFIX}locales`, + plugins: [ + new ExpirationPlugin({ + maxAgeSeconds: 30 * 24 * 60 * 60, // 1 month + maxEntries: 5, + }), + ], + }), +); + +registerRoute( + ({ request }) => request.destination === 'font', + new CacheFirst({ + cacheName: `${CACHE_NAME_PREFIX}fonts`, + plugins: [ + new ExpirationPlugin({ + maxAgeSeconds: 30 * 24 * 60 * 60, // 1 month + maxEntries: 5, + }), + ], + }), +); + +registerRoute( + ({ request }) => ['audio', 'image', 'track', 'video'].includes(request.destination), + new CacheFirst({ + cacheName: `m${CACHE_NAME_PREFIX}media`, + plugins: [ + new ExpirationPlugin({ + maxAgeSeconds: 7 * 24 * 60 * 60, // 1 week + maxEntries: 256, + }), + ], + }), +); // Cause a new version of a registered Service Worker to replace an existing one // that is already installed, and replace the currently active worker on open pages. @@ -52,26 +91,8 @@ self.addEventListener('fetch', function(event) { return response; })); - } /* else if (storageFreeable && (ATTACHMENT_HOST ? url.host === ATTACHMENT_HOST : url.pathname.startsWith('/system/'))) { - event.respondWith(openSystemCache().then(cache => { - return cache.match(event.request.url).then(cached => { - if (cached === undefined) { - const asyncResponse = invalidOnlyIfCached && event.request.cache === 'only-if-cached' ? - fetch(event.request, { cache: 'no-cache' }) : fetch(event.request); - - return asyncResponse.then(response => { - if (response.ok) { - cache - .put(event.request.url, response.clone()) - .catch(()=>{}).then(freeStorage()).catch(); - } - - return response; - }); - } - - return cached; - }); - })); - } */ + } }); + +self.addEventListener('push', handlePush); +self.addEventListener('notificationclick', handleNotificationClick); diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js index 48a2be7e7..9b75e9b9d 100644 --- a/app/javascript/mastodon/service_worker/web_push_notifications.js +++ b/app/javascript/mastodon/service_worker/web_push_notifications.js @@ -75,7 +75,7 @@ const formatMessage = (messageId, locale, values = {}) => const htmlToPlainText = html => unescape(html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, '')); -const handlePush = (event) => { +export const handlePush = (event) => { const { access_token, notification_id, preferred_locale, title, body, icon } = event.data.json(); // Placeholder until more information can be loaded @@ -189,7 +189,7 @@ const openUrl = url => return self.clients.openWindow(url); }); -const handleNotificationClick = (event) => { +export const handleNotificationClick = (event) => { const reactToNotificationClick = new Promise((resolve, reject) => { if (event.action) { if (event.action === 'expand') { @@ -211,6 +211,3 @@ const handleNotificationClick = (event) => { event.waitUntil(reactToNotificationClick); }; - -self.addEventListener('push', handlePush); -self.addEventListener('notificationclick', handleNotificationClick); diff --git a/app/javascript/mastodon/storage/db.js b/app/javascript/mastodon/storage/db.js deleted file mode 100644 index 377a792a7..000000000 --- a/app/javascript/mastodon/storage/db.js +++ /dev/null @@ -1,27 +0,0 @@ -export default () => new Promise((resolve, reject) => { - // ServiceWorker is required to synchronize the login state. - // Microsoft Edge 17 does not support getAll according to: - // Catalog of standard and vendor APIs across browsers - Microsoft Edge Development - // https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?q=specName%3Aindexeddb - if (!('caches' in self && 'getAll' in IDBObjectStore.prototype)) { - reject(); - return; - } - - const request = indexedDB.open('mastodon'); - - request.onerror = reject; - request.onsuccess = ({ target }) => resolve(target.result); - - request.onupgradeneeded = ({ target }) => { - const accounts = target.result.createObjectStore('accounts', { autoIncrement: true }); - const statuses = target.result.createObjectStore('statuses', { autoIncrement: true }); - - accounts.createIndex('id', 'id', { unique: true }); - accounts.createIndex('moved', 'moved'); - - statuses.createIndex('id', 'id', { unique: true }); - statuses.createIndex('account', 'account'); - statuses.createIndex('reblog', 'reblog'); - }; -}); diff --git a/app/javascript/mastodon/storage/modifier.js b/app/javascript/mastodon/storage/modifier.js deleted file mode 100644 index 9fadabef4..000000000 --- a/app/javascript/mastodon/storage/modifier.js +++ /dev/null @@ -1,211 +0,0 @@ -import openDB from './db'; - -const accountAssetKeys = ['avatar', 'avatar_static', 'header', 'header_static']; -const storageMargin = 8388608; -const storeLimit = 1024; - -// navigator.storage is not present on: -// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.100 Safari/537.36 Edge/16.16299 -// estimate method is not present on Chrome 57.0.2987.98 on Linux. -export const storageFreeable = 'storage' in navigator && 'estimate' in navigator.storage; - -function openCache() { - // ServiceWorker and Cache API is not available on iOS 11 - // https://webkit.org/status/#specification-service-workers - return self.caches ? caches.open('mastodon-system') : Promise.reject(); -} - -function printErrorIfAvailable(error) { - if (error) { - console.warn(error); - } -} - -function put(name, objects, onupdate, oncreate) { - return openDB().then(db => (new Promise((resolve, reject) => { - const putTransaction = db.transaction(name, 'readwrite'); - const putStore = putTransaction.objectStore(name); - const putIndex = putStore.index('id'); - - objects.forEach(object => { - putIndex.getKey(object.id).onsuccess = retrieval => { - function addObject() { - putStore.add(object); - } - - function deleteObject() { - putStore.delete(retrieval.target.result).onsuccess = addObject; - } - - if (retrieval.target.result) { - if (onupdate) { - onupdate(object, retrieval.target.result, putStore, deleteObject); - } else { - deleteObject(); - } - } else { - if (oncreate) { - oncreate(object, addObject); - } else { - addObject(); - } - } - }; - }); - - putTransaction.oncomplete = () => { - const readTransaction = db.transaction(name, 'readonly'); - const readStore = readTransaction.objectStore(name); - const count = readStore.count(); - - count.onsuccess = () => { - const excess = count.result - storeLimit; - - if (excess > 0) { - const retrieval = readStore.getAll(null, excess); - - retrieval.onsuccess = () => resolve(retrieval.result); - retrieval.onerror = reject; - } else { - resolve([]); - } - }; - - count.onerror = reject; - }; - - putTransaction.onerror = reject; - })).then(resolved => { - db.close(); - return resolved; - }, error => { - db.close(); - throw error; - })); -} - -function evictAccountsByRecords(records) { - return openDB().then(db => { - const transaction = db.transaction(['accounts', 'statuses'], 'readwrite'); - const accounts = transaction.objectStore('accounts'); - const accountsIdIndex = accounts.index('id'); - const accountsMovedIndex = accounts.index('moved'); - const statuses = transaction.objectStore('statuses'); - const statusesIndex = statuses.index('account'); - - function evict(toEvict) { - toEvict.forEach(record => { - openCache() - .then(cache => accountAssetKeys.forEach(key => cache.delete(records[key]))) - .catch(printErrorIfAvailable); - - accountsMovedIndex.getAll(record.id).onsuccess = ({ target }) => evict(target.result); - - statusesIndex.getAll(record.id).onsuccess = - ({ target }) => evictStatusesByRecords(target.result); - - accountsIdIndex.getKey(record.id).onsuccess = - ({ target }) => target.result && accounts.delete(target.result); - }); - } - - evict(records); - - db.close(); - }).catch(printErrorIfAvailable); -} - -export function evictStatus(id) { - evictStatuses([id]); -} - -export function evictStatuses(ids) { - return openDB().then(db => { - const transaction = db.transaction('statuses', 'readwrite'); - const store = transaction.objectStore('statuses'); - const idIndex = store.index('id'); - const reblogIndex = store.index('reblog'); - - ids.forEach(id => { - reblogIndex.getAllKeys(id).onsuccess = - ({ target }) => target.result.forEach(reblogKey => store.delete(reblogKey)); - - idIndex.getKey(id).onsuccess = - ({ target }) => target.result && store.delete(target.result); - }); - - db.close(); - }).catch(printErrorIfAvailable); -} - -function evictStatusesByRecords(records) { - return evictStatuses(records.map(({ id }) => id)); -} - -export function putAccounts(records, avatarStatic) { - const avatarKey = avatarStatic ? 'avatar_static' : 'avatar'; - const newURLs = []; - - put('accounts', records, (newRecord, oldKey, store, oncomplete) => { - store.get(oldKey).onsuccess = ({ target }) => { - accountAssetKeys.forEach(key => { - const newURL = newRecord[key]; - const oldURL = target.result[key]; - - if (newURL !== oldURL) { - openCache() - .then(cache => cache.delete(oldURL)) - .catch(printErrorIfAvailable); - } - }); - - const newURL = newRecord[avatarKey]; - const oldURL = target.result[avatarKey]; - - if (newURL !== oldURL) { - newURLs.push(newURL); - } - - oncomplete(); - }; - }, (newRecord, oncomplete) => { - newURLs.push(newRecord[avatarKey]); - oncomplete(); - }).then(records => Promise.all([ - evictAccountsByRecords(records), - openCache().then(cache => cache.addAll(newURLs)), - ])).then(freeStorage, error => { - freeStorage(); - throw error; - }).catch(printErrorIfAvailable); -} - -export function putStatuses(records) { - put('statuses', records) - .then(evictStatusesByRecords) - .catch(printErrorIfAvailable); -} - -export function freeStorage() { - return storageFreeable && navigator.storage.estimate().then(({ quota, usage }) => { - if (usage + storageMargin < quota) { - return null; - } - - return openDB().then(db => new Promise((resolve, reject) => { - const retrieval = db.transaction('accounts', 'readonly').objectStore('accounts').getAll(null, 1); - - retrieval.onsuccess = () => { - if (retrieval.result.length > 0) { - resolve(evictAccountsByRecords(retrieval.result).then(freeStorage)); - } else { - resolve(caches.delete('mastodon-system')); - } - }; - - retrieval.onerror = reject; - - db.close(); - })); - }); -} diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 431b8a73a..39211910f 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -190,6 +190,55 @@ a.table-action-link { } } + &__select-all { + background: $ui-base-color; + height: 47px; + align-items: center; + justify-content: center; + border: 1px solid darken($ui-base-color, 8%); + border-top: 0; + color: $secondary-text-color; + display: none; + + &.active { + display: flex; + } + + .selected, + .not-selected { + display: none; + + &.active { + display: block; + } + } + + strong { + font-weight: 700; + } + + span { + padding: 8px; + display: inline-block; + } + + button { + background: transparent; + border: 0; + font: inherit; + color: $highlight-text-color; + border-radius: 4px; + font-weight: 700; + padding: 8px; + + &:hover, + &:focus, + &:active { + background: lighten($ui-base-color, 8%); + } + } + } + &__form { padding: 16px; border: 1px solid darken($ui-base-color, 8%); |