diff --git a/package.json b/package.json index 654a712d..b8c27ff5 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "build-vercel-json": "node -r esm bin/build-vercel-json.js" }, "dependencies": { + "@formatjs/intl-listformat": "^5.0.10", "@formatjs/intl-locale": "^2.4.14", "@formatjs/intl-pluralrules": "^4.0.6", "@formatjs/intl-relativetimeformat": "^8.0.4", diff --git a/src/client.js b/src/client.js index 7bb5a668..2026e1fb 100644 --- a/src/client.js +++ b/src/client.js @@ -4,8 +4,8 @@ import './routes/_utils/historyEvents' import './routes/_utils/loadingMask' import './routes/_utils/forceOnline' import { mark, stop } from './routes/_utils/marks' -import { loadPolyfills } from './routes/_utils/loadPolyfills' -import { loadNonCriticalPolyfills } from './routes/_utils/loadNonCriticalPolyfills' +import { loadPolyfills } from './routes/_utils/polyfills/loadPolyfills' +import { loadNonCriticalPolyfills } from './routes/_utils/polyfills/loadNonCriticalPolyfills' mark('loadPolyfills') loadPolyfills().then(() => { diff --git a/src/intl/en-US.js b/src/intl/en-US.js index 43697d8c..d6546885 100644 --- a/src/intl/en-US.js +++ b/src/intl/en-US.js @@ -214,9 +214,11 @@ export default { thirtyMinutes: '30 minutes', oneHour: '1 hour', sixHours: '6 hours', + twelveHours: '12 hours', oneDay: '1 day', threeDays: '3 days', sevenDays: '7 days', + never: 'Never', addEmoji: 'Insert emoji', addMedia: 'Add media (images, video, audio)', addPoll: 'Add poll', @@ -625,5 +627,28 @@ export default { showingOfflineContent: 'Internet request failed. Showing offline content.', youAreOffline: 'You seem to be offline. You can still read toots while offline.', // Snackbar UI - updateAvailable: 'App update available.' + updateAvailable: 'App update available.', + // Word/phrase filters + wordFilters: 'Word filters', + noFilters: 'You don\'t have any word filters.', + wordOrPhrase: 'Word or phrase', + contexts: 'Contexts', + addFilter: 'Add filter', + editFilter: 'Edit filter', + filterHome: 'Home and lists', + filterNotifications: 'Notifications', + filterPublic: 'Public timelines', + filterThread: 'Conversations', + filterAccount: 'Profiles', + filterUnknown: 'Unknown', + expireAfter: 'Expire after', + whereToFilter: 'Where to filter', + irreversible: 'Irreversible', + wholeWord: 'Whole word', + save: 'Save', + updatedFilter: 'Updated filter', + createdFilter: 'Created filter', + failedToModifyFilter: 'Failed to modify filter: {error}', + deletedFilter: 'Deleted filter', + required: 'Required' } diff --git a/src/routes/_actions/addStatusOrNotification.js b/src/routes/_actions/addStatusOrNotification.js index 731cf116..76eb558c 100644 --- a/src/routes/_actions/addStatusOrNotification.js +++ b/src/routes/_actions/addStatusOrNotification.js @@ -31,9 +31,9 @@ async function insertUpdatesIntoTimeline (instanceName, timelineName, updates) { console.log('itemSummariesToAdd', JSON.parse(JSON.stringify(itemSummariesToAdd))) console.log('updates.map(timelineItemToSummary)', JSON.parse(JSON.stringify(updates.map(timelineItemToSummary)))) console.log('concat(itemSummariesToAdd, updates.map(timelineItemToSummary))', - JSON.parse(JSON.stringify(concat(itemSummariesToAdd, updates.map(timelineItemToSummary))))) + JSON.parse(JSON.stringify(concat(itemSummariesToAdd, updates.map(item => timelineItemToSummary(item, instanceName)))))) const newItemSummariesToAdd = uniqBy( - concat(itemSummariesToAdd, updates.map(timelineItemToSummary)), + concat(itemSummariesToAdd, updates.map(item => timelineItemToSummary(item, instanceName))), _ => _.id ) if (!isEqual(itemSummariesToAdd, newItemSummariesToAdd)) { @@ -78,7 +78,7 @@ async function insertUpdatesIntoThreads (instanceName, updates) { continue } const newItemSummariesToAdd = uniqBy( - concat(itemSummariesToAdd, validUpdates.map(timelineItemToSummary)), + concat(itemSummariesToAdd, validUpdates.map(item => timelineItemToSummary(item, instanceName))), _ => _.id ) if (!isEqual(itemSummariesToAdd, newItemSummariesToAdd)) { diff --git a/src/routes/_actions/filters.js b/src/routes/_actions/filters.js new file mode 100644 index 00000000..d59ca455 --- /dev/null +++ b/src/routes/_actions/filters.js @@ -0,0 +1,63 @@ +import { store } from '../_store/store' +import { createFilter, getFilters, updateFilter, deleteFilter as doDeleteFilter } from '../_api/filters' +import { cacheFirstUpdateAfter, cacheFirstUpdateOnlyIfNotInCache } from '../_utils/sync' +import { database } from '../_database/database' +import { isEqual } from 'lodash-es' +import { toast } from '../_components/toast/toast' +import { formatIntl } from '../_utils/formatIntl' +import { emit } from '../_utils/eventBus' + +async function syncFilters (instanceName, syncMethod) { + const { loggedInInstances } = store.get() + const accessToken = loggedInInstances[instanceName].access_token + + await syncMethod( + () => getFilters(instanceName, accessToken), + () => database.getFilters(instanceName), + filters => database.setFilters(instanceName, filters), + filters => { + const { instanceFilters } = store.get() + if (!isEqual(instanceFilters[instanceName], filters)) { // avoid re-render if nothing changed + instanceFilters[instanceName] = filters + store.set({ instanceFilters }) + } + } + ) +} + +export async function updateFiltersForInstance (instanceName) { + await syncFilters(instanceName, cacheFirstUpdateAfter) +} + +export async function setupFiltersForInstance (instanceName) { + await syncFilters(instanceName, cacheFirstUpdateOnlyIfNotInCache) +} + +export async function createOrUpdateFilter (instanceName, filter) { + const { loggedInInstances } = store.get() + const accessToken = loggedInInstances[instanceName].access_token + try { + if (filter.id) { + await updateFilter(instanceName, accessToken, filter) + /* no await */ toast.say('intl.updatedFilter') + } else { + await createFilter(instanceName, accessToken, filter) + /* no await */ toast.say('intl.createdFilter') + } + emit('wordFiltersChanged', instanceName) + } catch (err) { + /* no await */ toast.say(formatIntl('intl.failedToModifyFilter', err.message || '')) + } +} + +export async function deleteFilter (instanceName, id) { + const { loggedInInstances } = store.get() + const accessToken = loggedInInstances[instanceName].access_token + try { + await doDeleteFilter(instanceName, accessToken, id) + /* no await */ toast.say('intl.deletedFilter') + emit('wordFiltersChanged', instanceName) + } catch (err) { + /* no await */ toast.say(formatIntl('intl.failedToModifyFilter', err.message || '')) + } +} diff --git a/src/routes/_actions/stream/processMessage.js b/src/routes/_actions/stream/processMessage.js index 0368242b..4e66a052 100644 --- a/src/routes/_actions/stream/processMessage.js +++ b/src/routes/_actions/stream/processMessage.js @@ -1,8 +1,9 @@ import { mark, stop } from '../../_utils/marks' import { deleteStatus } from '../deleteStatuses' import { addStatusOrNotification } from '../addStatusOrNotification' +import { emit } from '../../_utils/eventBus' -const KNOWN_EVENTS = ['update', 'delete', 'notification', 'conversation'] +const KNOWN_EVENTS = ['update', 'delete', 'notification', 'conversation', 'filters_changed'] export function processMessage (instanceName, timelineName, message) { let { event, payload } = (message || {}) @@ -36,6 +37,9 @@ export function processMessage (instanceName, timelineName, message) { // It will add new DMs as new conversations instead of updating existing threads addStatusOrNotification(instanceName, timelineName, payload.last_status) break + case 'filters_changed': + emit('wordFiltersChanged', instanceName) + break } stop('processMessage') } diff --git a/src/routes/_actions/timeline.js b/src/routes/_actions/timeline.js index 4efb7b1a..900fabfc 100644 --- a/src/routes/_actions/timeline.js +++ b/src/routes/_actions/timeline.js @@ -98,7 +98,7 @@ async function fetchTimelineItemsFromNetwork (instanceName, accessToken, timelin async function addPagedTimelineItems (instanceName, timelineName, items) { console.log('addPagedTimelineItems, length:', items.length) mark('addPagedTimelineItemSummaries') - const newSummaries = items.map(timelineItemToSummary) + const newSummaries = items.map(item => timelineItemToSummary(item, instanceName)) await addPagedTimelineItemSummaries(instanceName, timelineName, newSummaries) stop('addPagedTimelineItemSummaries') } @@ -154,7 +154,7 @@ async function fetchTimelineItems (instanceName, accessToken, timelineName, onli async function addTimelineItems (instanceName, timelineName, items, stale) { console.log('addTimelineItems, length:', items.length) mark('addTimelineItemSummaries') - const newSummaries = items.map(timelineItemToSummary) + const newSummaries = items.map(item => timelineItemToSummary(item, instanceName)) addTimelineItemSummaries(instanceName, timelineName, newSummaries, stale) stop('addTimelineItemSummaries') } diff --git a/src/routes/_api/filters.js b/src/routes/_api/filters.js new file mode 100644 index 00000000..98ec6050 --- /dev/null +++ b/src/routes/_api/filters.js @@ -0,0 +1,22 @@ +import { get, DEFAULT_TIMEOUT, post, WRITE_TIMEOUT, put, del } from '../_utils/ajax' +import { auth, basename } from './utils' + +export function getFilters (instanceName, accessToken) { + const url = `${basename(instanceName)}/api/v1/filters` + return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT }) +} + +export function createFilter (instanceName, accessToken, filter) { + const url = `${basename(instanceName)}/api/v1/filters` + return post(url, filter, auth(accessToken), { timeout: WRITE_TIMEOUT }) +} + +export function updateFilter (instanceName, accessToken, filter) { + const url = `${basename(instanceName)}/api/v1/filters/${filter.id}` + return put(url, filter, auth(accessToken), { timeout: WRITE_TIMEOUT }) +} + +export function deleteFilter (instanceName, accessToken, id) { + const url = `${basename(instanceName)}/api/v1/filters/${id}` + return del(url, auth(accessToken), { timeout: WRITE_TIMEOUT }) +} diff --git a/src/routes/_components/dialog/asyncDialogs/importShowWordFilterDialog.js b/src/routes/_components/dialog/asyncDialogs/importShowWordFilterDialog.js new file mode 100644 index 00000000..fd779623 --- /dev/null +++ b/src/routes/_components/dialog/asyncDialogs/importShowWordFilterDialog.js @@ -0,0 +1,3 @@ +export const importShowWordFilterDialog = () => import( + /* webpackChunkName: 'showWordFilterDialog' */ '../creators/showWordFilterDialog' +).then(mod => mod.default) diff --git a/src/routes/_components/dialog/components/EmojiDialog.html b/src/routes/_components/dialog/components/EmojiDialog.html index 43253498..4a988916 100644 --- a/src/routes/_components/dialog/components/EmojiDialog.html +++ b/src/routes/_components/dialog/components/EmojiDialog.html @@ -66,7 +66,7 @@ import { doubleRAF } from '../../../_utils/doubleRAF' import { convertCustomEmojiToEmojiPickerFormat } from '../../../_utils/convertCustomEmojiToEmojiPickerFormat' import { supportsFocusVisible } from '../../../_utils/supportsFocusVisible' - import { importFocusVisible } from '../../../_utils/asyncPolyfills' + import { importFocusVisible } from '../../../_utils/polyfills/asyncPolyfills' import { emojiPickerI18n, emojiPickerDataSource, emojiPickerLocale } from '../../../_static/emojiPickerIntl' export default { diff --git a/src/routes/_components/dialog/components/GenericConfirmationDialog.html b/src/routes/_components/dialog/components/GenericConfirmationDialog.html index 10fad524..b7e5e980 100644 --- a/src/routes/_components/dialog/components/GenericConfirmationDialog.html +++ b/src/routes/_components/dialog/components/GenericConfirmationDialog.html @@ -9,7 +9,7 @@