feat: implement wellness settings (#1256)

* implement wellness settings

fixes #1192

Adds
- grayscale mode (as well as separate grayscale/dark grayscale
themes)
- disable follower/boost/fav counts (follower counts capped at 10)
- disable unread notification count (red dot)

* fix lint

* fix crawler
This commit is contained in:
Nolan Lawson 2019-06-01 13:07:31 -07:00 committed by GitHub
parent 27864fc47f
commit a35f5ee2d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 327 additions and 57 deletions

View file

@ -11,7 +11,6 @@ const render = promisify(sass.render.bind(sass))
const globalScss = path.join(__dirname, '../src/scss/global.scss')
const defaultThemeScss = path.join(__dirname, '../src/scss/themes/_default.scss')
const offlineThemeScss = path.join(__dirname, '../src/scss/themes/_offline.scss')
const customScrollbarScss = path.join(__dirname, '../src/scss/custom-scrollbars.scss')
const themesScssDir = path.join(__dirname, '../src/scss/themes')
const assetsDir = path.join(__dirname, '../static')
@ -22,11 +21,9 @@ async function renderCss (file) {
async function compileGlobalSass () {
let mainStyle = (await Promise.all([defaultThemeScss, globalScss].map(renderCss))).join('')
let offlineStyle = (await renderCss(offlineThemeScss))
let scrollbarStyle = (await renderCss(customScrollbarScss))
return `<style>\n${mainStyle}</style>\n` +
`<style media="only x" id="theOfflineStyle">\n${offlineStyle}</style>\n` +
`<style media="all" id="theScrollbarStyle">\n${scrollbarStyle}</style>\n`
}

View file

@ -51,5 +51,6 @@ module.exports = [
{ id: 'fa-bar-chart', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bar-chart.svg' },
{ id: 'fa-clock', src: 'src/thirdparty/font-awesome-svg-png/white/svg/clock-o.svg' },
{ id: 'fa-refresh', src: 'src/thirdparty/font-awesome-svg-png/white/svg/refresh.svg' },
{ id: 'fa-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/plus.svg' }
{ id: 'fa-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/plus.svg' },
{ id: 'fa-info-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/info-circle.svg' }
]

View file

@ -3,16 +3,21 @@
// To allow CSP to work correctly, we also calculate a sha256 hash during
// the build process and write it to checksum.js.
import { testHasLocalStorageOnce } from '../routes/_utils/testStorage'
import { INLINE_THEME, DEFAULT_THEME, switchToTheme } from '../routes/_utils/themeEngine'
import { basename } from '../routes/_api/utils'
import { onUserIsLoggedOut } from '../routes/_actions/onUserIsLoggedOut'
import { storeLite } from '../routes/_store/storeLite'
window.__themeColors = process.env.THEME_COLORS
const safeParse = str => (typeof str === 'undefined' || str === 'undefined') ? undefined : JSON.parse(str)
const hasLocalStorage = testHasLocalStorageOnce()
const currentInstance = hasLocalStorage && safeParse(localStorage.store_currentInstance)
const {
currentInstance,
instanceThemes,
disableCustomScrollbars,
enableGrayscale
} = storeLite.get()
const theme = (instanceThemes && instanceThemes[currentInstance]) || DEFAULT_THEME
if (currentInstance) {
// Do prefetch if we're logged in, so we can connect faster to the other origin.
@ -26,24 +31,23 @@ if (currentInstance) {
document.head.appendChild(link)
}
let theme = (currentInstance &&
localStorage.store_instanceThemes &&
safeParse(localStorage.store_instanceThemes)[safeParse(localStorage.store_currentInstance)]) ||
DEFAULT_THEME
if (theme !== INLINE_THEME) {
// switch theme ASAP to minimize flash of default theme
switchToTheme(theme)
switchToTheme(theme, enableGrayscale)
}
if (!hasLocalStorage || !currentInstance) {
if (enableGrayscale) {
document.body.classList.add('grayscale')
}
if (!currentInstance) {
// if not logged in, show all these 'hidden-from-ssr' elements
onUserIsLoggedOut()
}
if (hasLocalStorage && localStorage.store_disableCustomScrollbars === 'true') {
// if user has disabled custom scrollbars, remove this style
let theScrollbarStyle = document.getElementById('theScrollbarStyle')
theScrollbarStyle.setAttribute('media', 'only x') // disables the style
if (disableCustomScrollbars) {
document.getElementById('theScrollbarStyle')
.setAttribute('media', 'only x') // disables the style
}
// hack to make the scrollbars rounded only on macOS

View file

@ -84,7 +84,8 @@ async function registerNewInstance (code) {
instanceThemes: instanceThemes
})
store.save()
switchToTheme(DEFAULT_THEME)
let { enableGrayscale } = store.get()
switchToTheme(DEFAULT_THEME, enableGrayscale)
// fire off these requests so they're cached
/* no await */ updateVerifyCredentialsForInstance(currentRegisteredInstanceName)
/* no await */ updateCustomEmojiForInstance(currentRegisteredInstanceName)

View file

@ -1,6 +1,6 @@
import { getVerifyCredentials } from '../_api/user'
import { store } from '../_store/store'
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
import { switchToTheme } from '../_utils/themeEngine'
import { toast } from '../_components/toast/toast'
import { goto } from '../../../__sapper__/client'
import { cacheFirstUpdateAfter } from '../_utils/sync'
@ -14,7 +14,8 @@ export function changeTheme (instanceName, newTheme) {
store.save()
let { currentInstance } = store.get()
if (instanceName === currentInstance) {
switchToTheme(newTheme)
let { enableGrayscale } = store.get()
switchToTheme(newTheme, enableGrayscale)
}
}
@ -26,7 +27,8 @@ export function switchToInstance (instanceName) {
queryInSearch: ''
})
store.save()
switchToTheme(instanceThemes[instanceName])
let { enableGrayscale } = store.get()
switchToTheme(instanceThemes[instanceName], enableGrayscale)
}
export async function logOutOfInstance (instanceName) {
@ -55,7 +57,8 @@ export async function logOutOfInstance (instanceName) {
})
store.save()
toast.say(`Logged out of ${instanceName}`)
switchToTheme(instanceThemes[newInstance] || DEFAULT_THEME)
let { enableGrayscale } = store.get()
switchToTheme(instanceThemes[newInstance], enableGrayscale)
/* no await */ database.clearDatabaseForInstance(instanceName)
goto('/settings/instances')
}

View file

@ -127,7 +127,12 @@
numFollowers: ({ account }) => account.followers_count || 0,
numStatusesDisplay: ({ numStatuses }) => numberFormat.format(numStatuses),
numFollowingDisplay: ({ numFollowing }) => numberFormat.format(numFollowing),
numFollowersDisplay: ({ numFollowers }) => numberFormat.format(numFollowers),
numFollowersDisplay: ({ numFollowers, $disableFollowerCounts }) => {
if ($disableFollowerCounts && numFollowers >= 10) {
return '10+'
}
return numberFormat.format(numFollowers)
},
followersLabel: ({ numFollowers }) => `Followed by ${numFollowers}`,
followingLabel: ({ numFollowing }) => `Follows ${numFollowing}`
},

View file

@ -158,13 +158,19 @@
application: ({ originalStatus }) => originalStatus.application,
applicationName: ({ application }) => (application && application.name),
applicationWebsite: ({ application }) => (application && application.website),
numReblogs: ({ overrideNumReblogs, originalStatus }) => {
numReblogs: ({ $disableReblogCounts, overrideNumReblogs, originalStatus }) => {
if ($disableReblogCounts) {
return 0
}
if (typeof overrideNumReblogs === 'number') {
return overrideNumReblogs
}
return originalStatus.reblogs_count || 0
},
numFavs: ({ overrideNumFavs, originalStatus }) => {
numFavs: ({ $disableFavCounts, overrideNumFavs, originalStatus }) => {
if ($disableFavCounts) {
return 0
}
if (typeof overrideNumFavs === 'number') {
return overrideNumFavs
}
@ -173,13 +179,19 @@
displayAbsoluteFormattedDate: ({ createdAtDateTS, $isMobileSize }) => (
($isMobileSize ? shortAbsoluteDateFormatter : absoluteDateFormatter).format(createdAtDateTS)
),
reblogsLabel: ({ numReblogs }) => {
reblogsLabel: ({ $disableReblogCounts, numReblogs }) => {
if ($disableReblogCounts) {
return 'Boost counts hidden'
}
// TODO: intl
return numReblogs === 1
? `Boosted ${numReblogs} time`
: `Boosted ${numReblogs} times`
},
favoritesLabel: ({ numFavs }) => {
favoritesLabel: ({ $disableFavCounts, numFavs }) => {
if ($disableFavCounts) {
return 'Favorite counts hidden'
}
// TODO: intl
return numFavs === 1
? `Favorited ${numFavs} time`

View file

@ -8,6 +8,9 @@
<SettingsListRow>
<SettingsListButton href="/settings/instances" label="Instances"/>
</SettingsListRow>
<SettingsListRow>
<SettingsListButton href="/settings/wellness" label="Wellness"/>
</SettingsListRow>
<SettingsListRow>
<SettingsListButton href="/settings/hotkeys" label="Hotkeys"/>
</SettingsListRow>

View file

@ -0,0 +1,154 @@
<SettingsLayout page='settings/general' label="General">
<h1>Wellness Settings</h1>
<p>
Wellness settings are designed to reduce the addictive or anxiety-inducing aspects of social media.
Choose any options that work well for you.
</p>
<form class="ui-settings">
<div class="setting-group">
<input type="checkbox" id="choice-check-all"
on:change="onCheckAllChange(event)">
<label for="choice-check-all">Enable all</label>
</div>
</form>
<h2>Metrics</h2>
<form class="ui-settings">
<div class="setting-group">
<input type="checkbox" id="choice-disable-follower-counts"
bind:checked="$disableFollowerCounts" on:change="onChange(event)">
<label for="choice-disable-follower-counts">
Hide follower counts (capped at 10)
</label>
</div>
<div class="setting-group">
<input type="checkbox" id="choice-disable-reblog-counts"
bind:checked="$disableReblogCounts" on:change="onChange(event)">
<label for="choice-disable-reblog-counts">Hide boost counts</label>
</div>
<div class="setting-group">
<input type="checkbox" id="choice-disable-fav-counts"
bind:checked="$disableFavCounts" on:change="onChange(event)">
<label for="choice-disable-fav-counts">Hide favorite counts</label>
</div>
</form>
<h2>Notifications</h2>
<form class="ui-settings">
<div class="setting-group">
<input type="checkbox" id="choice-disable-unread-notification-counts"
bind:checked="$disableNotificationBadge" on:change="onChange(event)">
<label for="choice-disable-unread-notification-counts">
Hide unread notifications count (i.e. the red dot)
</label>
</div>
</form>
<aside>
<SvgIcon href="#fa-info-circle" className="aside-icon" />
<span>
You can filter or disable notifications in the
<a rel="prefetch" href="/settings/instances{$currentInstance ? '/' + $currentInstance : ''}">instance settings</a>.
</span>
</aside>
<h2>UI</h2>
<form class="ui-settings">
<div class="setting-group">
<input type="checkbox" id="choice-grayscale"
bind:checked="$enableGrayscale" on:change="onChange(event)">
<label for="choice-grayscale">Grayscale mode</label>
</div>
</form>
<p>
These settings are partly based on guidelines from the
<ExternalLink href="https://humanetech.com">Center for Humane Technology</ExternalLink>.
</p>
</SettingsLayout>
<style>
.ui-settings {
background: var(--form-bg);
border: 1px solid var(--main-border);
border-radius: 4px;
padding: 20px;
line-height: 2em;
}
.setting-group {
padding: 5px 0;
}
aside {
font-size: 1.2em;
margin: 20px 10px 0px 10px;
color: var(--deemphasized-text-color);
display: flex;
align-items: center;
}
aside a {
text-decoration: underline;
color: var(--deemphasized-text-color);
}
aside span {
flex: 1;
}
:global(.aside-icon) {
fill: var(--deemphasized-text-color);
width: 18px;
height: 18px;
margin: 0 10px 0 5px;
min-width: 18px;
}
</style>
<script>
import SettingsLayout from '../../_components/settings/SettingsLayout.html'
import { store } from '../../_store/store'
import ExternalLink from '../../_components/ExternalLink.html'
import SvgIcon from '../../_components/SvgIcon.html'
export default {
oncreate () {
this.flushChangesToCheckAll()
},
components: {
SettingsLayout,
ExternalLink,
SvgIcon
},
methods: {
flushChangesToCheckAll () {
const {
disableFollowerCounts,
disableReblogCounts,
disableFavCounts,
disableNotificationBadge,
enableGrayscale
} = this.store.get()
document.querySelector('#choice-check-all').checked = disableFollowerCounts &&
disableReblogCounts &&
disableFavCounts &&
disableNotificationBadge &&
enableGrayscale
},
onCheckAllChange (e) {
let { checked } = e.target
this.store.set({
disableFollowerCounts: checked,
disableReblogCounts: checked,
disableFavCounts: checked,
disableNotificationBadge: checked,
enableGrayscale: checked
})
this.store.save()
},
onChange () {
this.flushChangesToCheckAll()
this.store.save()
}
},
store: () => store
}
</script>

View file

@ -41,6 +41,12 @@ const themes = [
dark: false,
color: '#4ab92f'
},
{
name: 'grayscale',
label: 'Grayscale',
dark: false,
color: '#999999'
},
{
name: 'ozark',
label: 'Ozark',
@ -88,6 +94,12 @@ const themes = [
label: 'Pitch Black',
dark: true,
color: '#000'
},
{
name: 'dark-grayscale',
label: 'Dark Grayscale',
dark: true,
color: '#666'
}
]

View file

@ -1,10 +1,7 @@
import { Store } from 'svelte/store'
import { safeLocalStorage as LS } from '../_utils/safeLocalStorage'
import lifecycle from 'page-lifecycle/dist/lifecycle.mjs'
function safeParse (str) {
return !str ? undefined : (str === 'undefined' ? undefined : JSON.parse(str))
}
import { safeParse } from './safeParse'
export class LocalStorageStore extends Store {
constructor (state, keysToWatch) {

View file

@ -174,7 +174,9 @@ export function timelineComputations (store) {
)
store.compute('hasNotifications',
['numberOfNotifications', 'currentPage'],
(numberOfNotifications, currentPage) => currentPage !== 'notifications' && !!numberOfNotifications
['numberOfNotifications', 'currentPage', 'disableNotificationBadge'],
(numberOfNotifications, currentPage, $disableNotificationBadge) => (
!$disableNotificationBadge && currentPage !== 'notifications' && !!numberOfNotifications
)
)
}

View file

@ -0,0 +1,14 @@
import { switchToTheme } from '../../_utils/themeEngine'
export function grayscaleObservers (store) {
if (!process.browser) {
return
}
store.observe('enableGrayscale', enableGrayscale => {
const { instanceThemes, currentInstance } = store.get()
const theme = instanceThemes && instanceThemes[currentInstance]
document.body.classList.toggle('grayscale', enableGrayscale)
switchToTheme(theme, enableGrayscale)
})
}

View file

@ -6,6 +6,7 @@ import { resizeObservers } from './resizeObservers'
import { setupLoggedInObservers } from './setupLoggedInObservers'
import { logOutObservers } from './logOutObservers'
import { touchObservers } from './touchObservers'
import { grayscaleObservers } from './grayscaleObservers'
export function observers (store) {
onlineObservers(store)
@ -15,5 +16,6 @@ export function observers (store) {
resizeObservers(store)
touchObservers(store)
logOutObservers(store)
grayscaleObservers(store)
setupLoggedInObservers(store)
}

View file

@ -6,8 +6,6 @@ const NOTIFY_OFFLINE_LIMIT = 1
let notifyCount = 0
let offlineStyle = process.browser && document.getElementById('theOfflineStyle')
// debounce to avoid notifying for a short connection issue
const notifyOffline = debounce(() => {
if (process.browser && !navigator.onLine && ++notifyCount <= NOTIFY_OFFLINE_LIMIT) {
@ -19,20 +17,9 @@ export function onlineObservers (store) {
if (!process.browser) {
return
}
let meta = document.getElementById('theThemeColor')
let oldTheme = meta.content
store.observe('online', online => {
// "only x" ensures the <style> tag does not have any effect
offlineStyle.setAttribute('media', online ? 'only x' : 'all')
if (online) {
meta.content = oldTheme
} else {
let offlineThemeColor = window.__themeColors.offline
if (meta.content !== offlineThemeColor) {
oldTheme = meta.content
}
meta.content = offlineThemeColor
if (!online) {
notifyOffline()
}
})

View file

@ -0,0 +1,3 @@
export function safeParse (str) {
return !str ? undefined : (str === 'undefined' ? undefined : JSON.parse(str))
}

View file

@ -12,10 +12,15 @@ const persistedState = {
currentRegisteredInstance: undefined,
// we disable scrollbars by default on iOS
disableCustomScrollbars: process.browser && /iP(?:hone|ad|od)/.test(navigator.userAgent),
disableFavCounts: false,
disableFollowerCounts: false,
disableHotkeys: false,
disableInfiniteScroll: false,
disableLongAriaLabels: false,
disableNotificationBadge: false,
disableReblogCounts: false,
disableTapOnStatus: false,
enableGrayscale: false,
hideCards: false,
largeInlineMedia: false,
instanceNameInSearch: '',

View file

@ -0,0 +1,26 @@
// "lite" version of the store used in the inline script. Purely read-only,
// does not implement non-LocalStorage store features.
import { safeParse } from './safeParse'
import { testHasLocalStorageOnce } from '../_utils/testStorage'
const hasLocalStorage = testHasLocalStorageOnce()
export const storeLite = {
get () {
if (!hasLocalStorage) {
return {}
}
const res = {}
const LS = localStorage
for (let i = 0, len = LS.length; i < len; i++) {
let key = LS.key(i)
if (key.startsWith('store_')) {
let item = LS.getItem(key)
let value = safeParse(item)
res[key] = value
}
}
return res
}
}

View file

@ -1,6 +1,5 @@
let meta = process.browser && document.getElementById('theThemeColor')
let offlineStyle = process.browser && document.getElementById('theOfflineStyle')
let prefersDarkTheme = process.browser && window.matchMedia('(prefers-color-scheme: dark)').matches
const prefersDarkTheme = process.browser && window.matchMedia('(prefers-color-scheme: dark)').matches
const meta = process.browser && document.getElementById('theThemeColor')
export const INLINE_THEME = 'default' // theme that does not require external CSS
export const DEFAULT_LIGHT_THEME = 'default' // theme that is shown by default
@ -32,11 +31,13 @@ function loadCSS (href) {
}
})
// inserting before the offline <style> ensures that the offline style wins when offline
document.head.insertBefore(link, offlineStyle)
document.head.appendChild(link)
}
export function switchToTheme (themeName = DEFAULT_THEME) {
export function switchToTheme (themeName = DEFAULT_THEME, enableGrayscale) {
if (enableGrayscale) {
themeName = prefersDarkTheme ? 'grayscale-dark' : 'grayscale'
}
let themeColor = window.__themeColors[themeName]
meta.content = themeColor || window.__themeColors[DEFAULT_THEME]
if (themeName !== INLINE_THEME) {

View file

@ -0,0 +1,20 @@
<Title name="Wellness Settings" settingsPage={true} />
<LazyPage {pageComponent} {params} />
<script>
import Title from '../_components/Title.html'
import LazyPage from '../_components/LazyPage.html'
import pageComponent from '../_pages/settings/wellness.html'
export default {
components: {
Title,
LazyPage
},
data: () => ({
pageComponent
})
}
</script>

View file

@ -31,6 +31,11 @@ body {
background: var(--body-bg);
-webkit-tap-highlight-color: transparent; // fix for blue background on spoiler tap on Chrome for Android
overflow-x: hidden; // Prevent horizontal scrolling on mobile Firefox on small screens
&.grayscale {
filter: grayscale(100%);
}
}
.main-content {

View file

@ -0,0 +1,16 @@
$main-theme-color: #444;
$main-bg-color: #202020;
$body-bg-color: darken($main-bg-color, 5%);
$anchor-color: #999;
$main-text-color: #FFF;
$border-color: lighten($body-bg-color, 10%);
$secondary-text-color: white;
$toast-border: #fafafa;
$toast-bg: #333;
$focus-outline: lighten($main-theme-color, 50%);
$compose-background: lighten($main-theme-color, 52%);
@import "_base.scss";
@import "_dark.scss";
@import "_dark_navbar.scss";
@import "_dark_scrollbars.scss";

View file

@ -1,6 +1,6 @@
$main-theme-color: #999999;
$main-theme-color: #666;
$body-bg-color: lighten($main-theme-color, 38%);
$anchor-color: $main-theme-color;
$anchor-color: lighten($main-theme-color, 5%);
$main-text-color: #333;
$border-color: #dadada;
$main-bg-color: white;
@ -11,4 +11,4 @@ $focus-outline: lighten($main-theme-color, 15%);
$compose-background: lighten($main-theme-color, 17%);
@import "_base.scss";
@import "_light_scrollbars.scss";
@import "_light_scrollbars.scss";