Compare commits
16 Commits
210e942398
...
268a7ce3b6
Author | SHA1 | Date |
---|---|---|
'leftie | 268a7ce3b6 | |
'leftie | d9fba6d79d | |
Nolan Lawson | 155cb05e39 | |
Nolan Lawson | 5d0e95e759 | |
Nolan Lawson | 58a8772edc | |
Nolan Lawson | d75507bbce | |
Nolan Lawson | 604471a158 | |
Nolan Lawson | f5c7bc790f | |
Nolan Lawson | 74230cfe8e | |
Nolan Lawson | a35f5ee2d9 | |
Nolan Lawson | 27864fc47f | |
Nolan Lawson | fcf64c2169 | |
Nolan Lawson | 45630c185f | |
Nolan Lawson | 44a87dcd9a | |
Nolan Lawson | 8672ade314 | |
Nolan Lawson | fa2eb8fe52 |
|
@ -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`
|
||||
}
|
||||
|
||||
|
|
|
@ -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' }
|
||||
]
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "pinafore",
|
||||
"description": "Alternative web client for Mastodon",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0",
|
||||
"scripts": {
|
||||
"lint": "standard && standard --plugin html 'src/routes/**/*.html'",
|
||||
"lint-fix": "standard --fix && standard --fix --plugin html 'src/routes/**/*.html'",
|
||||
|
|
|
@ -17,6 +17,17 @@
|
|||
|
||||
<!-- inline CSS -->
|
||||
|
||||
<style id="theGrayscaleStyle" media="only x">
|
||||
/* Firefox doesn't seem to like applying filter: grayscale() to
|
||||
* the entire body, so we apply individually.
|
||||
*/
|
||||
img, svg, video,
|
||||
input[type="checkbox"], input[type="radio"],
|
||||
.inline-emoji, .theme-preview, .emoji-mart-emoji, .emoji-mart-skin {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
</style>
|
||||
|
||||
<noscript>
|
||||
<style>
|
||||
.hidden-from-ssr {
|
||||
|
|
|
@ -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,24 @@ 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.getElementById('theGrayscaleStyle')
|
||||
.setAttribute('media', 'all') // enables the style
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ export async function setupTimeline () {
|
|||
stop('setupTimeline')
|
||||
}
|
||||
|
||||
export async function fetchTimelineItemsOnScrollToBottom (instanceName, timelineName) {
|
||||
export async function fetchMoreItemsAtBottomOfTimeline (instanceName, timelineName) {
|
||||
console.log('setting runningUpdate: true')
|
||||
store.setForTimeline(instanceName, timelineName, { runningUpdate: true })
|
||||
await fetchTimelineItemsAndPossiblyFallBack()
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{#if hidePage}
|
||||
<LoadingPage />
|
||||
{/if}
|
||||
<ComposeBox realm="home" hidden={hidePage}/>
|
||||
<LazyComposeBox realm="home" hidden={hidePage}/>
|
||||
<div class="timeline-home-anchor-container">
|
||||
{#if !hidePage && hideTimeline}
|
||||
<LoadingPage />
|
||||
|
@ -29,7 +29,7 @@
|
|||
import LazyTimeline from './timeline/LazyTimeline.html'
|
||||
import { store } from '../_store/store.js'
|
||||
import LoadingPage from './LoadingPage.html'
|
||||
import ComposeBox from './compose/ComposeBox.html'
|
||||
import LazyComposeBox from './compose/LazyComposeBox.html'
|
||||
|
||||
export default {
|
||||
oncreate () {
|
||||
|
@ -44,9 +44,9 @@
|
|||
},
|
||||
store: () => store,
|
||||
components: {
|
||||
LazyComposeBox,
|
||||
LazyTimeline,
|
||||
LoadingPage,
|
||||
ComposeBox
|
||||
LoadingPage
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<div class="compose-box-toolbar">
|
||||
<div class="compose-box-toolbar-items">
|
||||
<IconButton
|
||||
className="compose-toolbar-button"
|
||||
label="Insert emoji"
|
||||
href="#fa-smile"
|
||||
on:click="onEmojiClick()"
|
||||
/>
|
||||
<IconButton
|
||||
className="compose-toolbar-button"
|
||||
svgClassName={$uploadingMedia ? 'spin' : ''}
|
||||
label="Add media"
|
||||
href={$uploadingMedia ? '#fa-spinner' : '#fa-camera'}
|
||||
|
@ -13,6 +15,7 @@
|
|||
disabled={$uploadingMedia || (media.length === 4)}
|
||||
/>
|
||||
<IconButton
|
||||
className="compose-toolbar-button"
|
||||
label="{poll && poll.options && poll.options.length ? 'Remove poll' : 'Add poll'}"
|
||||
href="#fa-bar-chart"
|
||||
on:click="onPollClick()"
|
||||
|
@ -20,11 +23,13 @@
|
|||
pressed={poll && poll.options && poll.options.length}
|
||||
/>
|
||||
<IconButton
|
||||
className="compose-toolbar-button"
|
||||
label="Adjust privacy (currently {postPrivacy.label})"
|
||||
href={postPrivacy.icon}
|
||||
on:click="onPostPrivacyClick()"
|
||||
/>
|
||||
<IconButton
|
||||
className="compose-toolbar-button"
|
||||
label={contentWarningShown ? 'Remove content warning' : 'Add content warning'}
|
||||
href="#fa-exclamation-triangle"
|
||||
on:click="onContentWarningClick()"
|
||||
|
@ -47,6 +52,13 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 320px) {
|
||||
:global(button.icon-button.compose-toolbar-button) {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import IconButton from '../IconButton.html'
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{#await importComposeBox}
|
||||
<!-- awaiting promise -->
|
||||
{:then ComposeBox}
|
||||
<svelte:component this={ComposeBox} {realm} {hidden} />
|
||||
{:catch error}
|
||||
<div>Component failed to load. Try refreshing! {error}</div>
|
||||
{/await}
|
||||
<script>
|
||||
import { importComposeBox } from '../../_utils/asyncModules'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
importComposeBox: importComposeBox()
|
||||
})
|
||||
}
|
||||
</script>
|
|
@ -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}`
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<a ref:cardlink href={url} class="status-card" target="_blank" rel="noopener noreferrer">
|
||||
<strong class="card-title">
|
||||
{title}
|
||||
{unescapedTitle}
|
||||
</strong>
|
||||
{#if description}
|
||||
<div class="card-content">
|
||||
|
@ -10,7 +10,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
<span class="card-description">
|
||||
{description}
|
||||
{unescapedDescription}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -87,6 +87,7 @@
|
|||
<script>
|
||||
import LazyImage from '../LazyImage.html'
|
||||
import Shortcut from '../shortcut/Shortcut.html'
|
||||
import { unescape } from '../../_thirdparty/unescape/unescape'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -96,9 +97,11 @@
|
|||
computed: {
|
||||
card: ({ originalStatus }) => originalStatus.card,
|
||||
title: ({ card }) => card.title,
|
||||
unescapedTitle: ({ title }) => title && unescape(title),
|
||||
url: ({ card }) => card.url,
|
||||
hostname: ({ url }) => window.URL ? new window.URL(url).hostname : '',
|
||||
description: ({ card, hostname }) => card.description || card.provider_name || hostname,
|
||||
unescapedDescription: ({ description }) => description && unescape(description),
|
||||
imageUrl: ({ card }) => card.image
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -1,32 +1,100 @@
|
|||
<div class="loading-footer {shown ? '' : 'hidden'}">
|
||||
<LoadingSpinner size={48} />
|
||||
<span class="loading-footer-info">
|
||||
Loading more...
|
||||
</span>
|
||||
<div class="loading-wrapper {showLoading ? 'shown' : ''}"
|
||||
aria-hidden={!showLoading}
|
||||
role="alert"
|
||||
>
|
||||
<LoadingSpinner size={48} />
|
||||
<span class="loading-footer-info">
|
||||
Loading more...
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-wrapper {showLoadButton ? 'shown' : ''}"
|
||||
aria-hidden={!showLoadButton}
|
||||
>
|
||||
<button type="button"
|
||||
class="primary"
|
||||
on:click="onClickLoadMore(event)">
|
||||
Load more
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.loading-footer {
|
||||
padding: 20px 0 10px;
|
||||
padding: 20px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
.loading-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.loading-wrapper.shown {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.loading-footer-info {
|
||||
margin-left: 20px;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
.button-wrapper {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: none;
|
||||
}
|
||||
.button-wrapper.shown {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transition: opacity 0.2s linear 0.2s;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
import LoadingSpinner from '../LoadingSpinner.html'
|
||||
import { store } from '../../_store/store'
|
||||
import { fetchMoreItemsAtBottomOfTimeline } from '../../_actions/timeline'
|
||||
|
||||
export default {
|
||||
store: () => store,
|
||||
computed: {
|
||||
shown: ({ $timelineInitialized, $runningUpdate }) => ($timelineInitialized && $runningUpdate)
|
||||
shown: ({ $timelineInitialized, $runningUpdate, $disableInfiniteScroll }) => (
|
||||
$timelineInitialized && ($disableInfiniteScroll || $runningUpdate)
|
||||
),
|
||||
showLoading: ({ $runningUpdate }) => $runningUpdate,
|
||||
showLoadButton: ({ $runningUpdate, $disableInfiniteScroll }) => !$runningUpdate && $disableInfiniteScroll
|
||||
},
|
||||
methods: {
|
||||
onClickLoadMore (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let { currentInstance, currentTimeline } = this.store.get()
|
||||
/* no await */ fetchMoreItemsAtBottomOfTimeline(currentInstance, currentTimeline)
|
||||
// focus the last item in the timeline; it makes the most sense to me since the button disappears
|
||||
try {
|
||||
// TODO: should probably expose this as an API on the virtual list instead of reaching into the DOM
|
||||
let virtualListItems = document.querySelector('.virtual-list').children
|
||||
let lastItem = virtualListItems[virtualListItems.length - 2] // -2 because the footer is last
|
||||
lastItem.querySelector('article').focus()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
LoadingSpinner
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
} from '../../_utils/asyncModules'
|
||||
import { timelines } from '../../_static/timelines'
|
||||
import {
|
||||
fetchTimelineItemsOnScrollToBottom,
|
||||
fetchMoreItemsAtBottomOfTimeline,
|
||||
setupTimeline,
|
||||
showMoreItemsForTimeline,
|
||||
showMoreItemsForThread,
|
||||
|
@ -130,8 +130,8 @@
|
|||
itemIds: ({ $filteredTimelineItemSummaries }) => (
|
||||
$filteredTimelineItemSummaries && $filteredTimelineItemSummaries.map(_ => _.id)
|
||||
),
|
||||
itemIdsToAdd: ({ $timelineItemSummariesToAdd }) => (
|
||||
$timelineItemSummariesToAdd && $timelineItemSummariesToAdd.map(_ => _.id)
|
||||
itemIdsToAdd: ({ $filteredTimelineItemSummariesToAdd }) => (
|
||||
$filteredTimelineItemSummariesToAdd && $filteredTimelineItemSummariesToAdd.map(_ => _.id)
|
||||
),
|
||||
headerProps: ({ itemIdsToAdd }) => {
|
||||
return {
|
||||
|
@ -165,18 +165,16 @@
|
|||
},
|
||||
onScrollToBottom () {
|
||||
let { timelineType } = this.get()
|
||||
let { timelineInitialized, runningUpdate } = this.store.get()
|
||||
let { timelineInitialized, runningUpdate, disableInfiniteScroll } = this.store.get()
|
||||
if (!timelineInitialized ||
|
||||
runningUpdate ||
|
||||
disableInfiniteScroll ||
|
||||
timelineType === 'status') { // for status contexts, we've already fetched the whole thread
|
||||
return
|
||||
}
|
||||
let { currentInstance } = this.store.get()
|
||||
let { timeline } = this.get()
|
||||
fetchTimelineItemsOnScrollToBottom(
|
||||
currentInstance,
|
||||
timeline
|
||||
)
|
||||
/* no await */ fetchMoreItemsAtBottomOfTimeline(currentInstance, timeline)
|
||||
},
|
||||
onScrollToTop () {
|
||||
let { shouldShowHeader } = this.store.get()
|
||||
|
@ -188,7 +186,7 @@
|
|||
}
|
||||
},
|
||||
setupStreaming () {
|
||||
let { currentInstance } = this.store.get()
|
||||
let { currentInstance, disableInfiniteScroll } = this.store.get()
|
||||
let { timeline, timelineType } = this.get()
|
||||
let handleItemIdsToAdd = () => {
|
||||
let { itemIdsToAdd } = this.get()
|
||||
|
@ -204,13 +202,17 @@
|
|||
if (timelineType === 'status') {
|
||||
// this is a thread, just insert the statuses already
|
||||
showMoreItemsForThread(currentInstance, timeline)
|
||||
} else if (scrollTop === 0 && !shouldShowHeader && !showHeader) {
|
||||
} else if (!disableInfiniteScroll && scrollTop === 0 && !shouldShowHeader && !showHeader) {
|
||||
// if the user is scrolled to the top and we're not showing the header, then
|
||||
// just insert the statuses. this is "chat room mode"
|
||||
showMoreItemsForTimeline(currentInstance, timeline)
|
||||
} else {
|
||||
// user hasn't scrolled to the top, show a header instead
|
||||
this.store.setForTimeline(currentInstance, timeline, { shouldShowHeader: true })
|
||||
// unless the user has disabled infinite scroll entirely
|
||||
if (disableInfiniteScroll) {
|
||||
this.store.setForTimeline(currentInstance, timeline, { showHeader: true })
|
||||
}
|
||||
}
|
||||
stop('handleItemIdsToAdd')
|
||||
}
|
||||
|
|
|
@ -32,6 +32,19 @@
|
|||
bind:checked="$disableCustomScrollbars" on:change="onChange(event)">
|
||||
<label for="choice-disable-custom-scrollbars">Disable custom scrollbars</label>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<input type="checkbox" id="choice-disable-infinite-scroll"
|
||||
bind:checked="$disableInfiniteScroll" on:change="onChange(event)">
|
||||
<label for="choice-disable-infinite-scroll">Disable
|
||||
<Tooltip
|
||||
text="infinite scroll"
|
||||
tooltipText={
|
||||
"When infinite scroll is disabled, new toots will not automatically appear at the bottom " +
|
||||
"or top of the timeline. Instead, buttons will allow you to load more content on demand."
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<input type="checkbox" id="choice-hide-cards"
|
||||
bind:checked="$hideCards" on:change="onChange(event)">
|
||||
|
@ -89,11 +102,13 @@
|
|||
import SettingsLayout from '../../_components/settings/SettingsLayout.html'
|
||||
import ThemeSettings from '../../_components/settings/instance/ThemeSettings.html'
|
||||
import { store } from '../../_store/store'
|
||||
import Tooltip from '../../_components/Tooltip.html'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SettingsLayout,
|
||||
ThemeSettings
|
||||
ThemeSettings,
|
||||
Tooltip
|
||||
},
|
||||
methods: {
|
||||
onChange (event) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -41,6 +41,12 @@ const themes = [
|
|||
dark: false,
|
||||
color: '#4ab92f'
|
||||
},
|
||||
{
|
||||
name: 'grayscale',
|
||||
label: 'Grayscale',
|
||||
dark: false,
|
||||
color: '#999999'
|
||||
},
|
||||
{
|
||||
name: 'sam',
|
||||
label: 'Sam',
|
||||
|
@ -112,6 +118,12 @@ const themes = [
|
|||
label: 'Pitch Black',
|
||||
dark: true,
|
||||
color: '#000'
|
||||
},
|
||||
{
|
||||
name: 'dark-grayscale',
|
||||
label: 'Dark Grayscale',
|
||||
dark: true,
|
||||
color: '#666'
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -143,6 +143,14 @@ export function timelineComputations (store) {
|
|||
}
|
||||
)
|
||||
|
||||
store.compute(
|
||||
'filteredTimelineItemSummariesToAdd',
|
||||
['timelineItemSummariesToAdd', 'timelineFilterFunction'],
|
||||
(timelineItemSummariesToAdd, timelineFilterFunction) => {
|
||||
return timelineItemSummariesToAdd && timelineItemSummariesToAdd.filter(timelineFilterFunction)
|
||||
}
|
||||
)
|
||||
|
||||
store.compute('timelineNotificationItemSummaries',
|
||||
[`timelineData_timelineItemSummariesToAdd`, 'timelineFilterFunction', 'currentInstance'],
|
||||
(root, timelineFilterFunction, currentInstance) => (
|
||||
|
@ -159,14 +167,18 @@ export function timelineComputations (store) {
|
|||
)
|
||||
|
||||
store.compute('numberOfNotifications',
|
||||
['filteredTimelineNotificationItemSummaries'],
|
||||
(filteredTimelineNotificationItemSummaries) => (
|
||||
filteredTimelineNotificationItemSummaries ? filteredTimelineNotificationItemSummaries.length : 0
|
||||
['filteredTimelineNotificationItemSummaries', 'disableNotificationBadge'],
|
||||
(filteredTimelineNotificationItemSummaries, disableNotificationBadge) => (
|
||||
(!disableNotificationBadge && filteredTimelineNotificationItemSummaries)
|
||||
? filteredTimelineNotificationItemSummaries.length
|
||||
: 0
|
||||
)
|
||||
)
|
||||
|
||||
store.compute('hasNotifications',
|
||||
['numberOfNotifications', 'currentPage'],
|
||||
(numberOfNotifications, currentPage) => currentPage !== 'notifications' && !!numberOfNotifications
|
||||
(numberOfNotifications, currentPage) => (
|
||||
currentPage !== 'notifications' && !!numberOfNotifications
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { switchToTheme } from '../../_utils/themeEngine'
|
||||
|
||||
const style = process.browser && document.getElementById('theGrayscaleStyle')
|
||||
|
||||
export function grayscaleObservers (store) {
|
||||
if (!process.browser) {
|
||||
return
|
||||
}
|
||||
|
||||
store.observe('enableGrayscale', enableGrayscale => {
|
||||
const { instanceThemes, currentInstance } = store.get()
|
||||
const theme = instanceThemes && instanceThemes[currentInstance]
|
||||
style.setAttribute('media', enableGrayscale ? 'all' : 'only x') // disable or enable the style
|
||||
switchToTheme(theme, enableGrayscale)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export function safeParse (str) {
|
||||
return !str ? undefined : (str === 'undefined' ? undefined : JSON.parse(str))
|
||||
}
|
|
@ -12,9 +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: '',
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
// "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 () {
|
||||
return new Proxy({}, {
|
||||
get: function (obj, prop) {
|
||||
if (!(prop in obj)) {
|
||||
obj[prop] = hasLocalStorage && safeParse(localStorage.getItem(`store_${prop}`))
|
||||
}
|
||||
return obj[prop]
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014, 2016-2017, Jon Schlinkert
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
|
@ -0,0 +1,63 @@
|
|||
// via https://github.com/jonschlinkert/unescape/blob/98d1e52/index.js
|
||||
|
||||
const chars = {
|
||||
'"': '"',
|
||||
'"': '"',
|
||||
|
||||
''': '\'',
|
||||
''': '\'',
|
||||
|
||||
'&': '&',
|
||||
'&': '&',
|
||||
|
||||
'>': '>',
|
||||
'>': '>',
|
||||
|
||||
'<': '<',
|
||||
'<': '<',
|
||||
|
||||
'¢': '¢',
|
||||
'¢': '¢',
|
||||
|
||||
'©': '©',
|
||||
'©': '©',
|
||||
|
||||
'€': '€',
|
||||
'€': '€',
|
||||
|
||||
'£': '£',
|
||||
'£': '£',
|
||||
|
||||
'®': '®',
|
||||
'®': '®',
|
||||
|
||||
'¥': '¥',
|
||||
'¥': '¥',
|
||||
|
||||
' ': ' '
|
||||
}
|
||||
|
||||
let regex
|
||||
|
||||
/**
|
||||
* Convert HTML entities to HTML characters.
|
||||
*
|
||||
* @param {String} `str` String with HTML entities to un-escape.
|
||||
* @return {String}
|
||||
*/
|
||||
|
||||
function unescape (str) {
|
||||
regex = regex || toRegex(chars)
|
||||
return str.replace(regex, m => chars[m])
|
||||
}
|
||||
|
||||
function toRegex (chars) {
|
||||
var keys = Object.keys(chars).join('|')
|
||||
return new RegExp('(' + keys + ')', 'g')
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose `unescape`
|
||||
*/
|
||||
|
||||
export { unescape }
|
|
@ -43,3 +43,7 @@ export const importToast = () => import(
|
|||
export const importSnackbar = () => import(
|
||||
/* webpackChunkName: 'Snackbar.html' */ '../_components/snackbar/Snackbar.html'
|
||||
).then(getDefault)
|
||||
|
||||
export const importComposeBox = () => import(
|
||||
/* webpackChunkName: 'ComposeBox.html' */ '../_components/compose/ComposeBox.html'
|
||||
).then(getDefault)
|
||||
|
|
|
@ -1,11 +1,22 @@
|
|||
import { snackbar } from '../_components/snackbar/snackbar'
|
||||
|
||||
async function skipWaiting () {
|
||||
const reg = await navigator.serviceWorker.getRegistration()
|
||||
if (!reg || !reg.waiting) {
|
||||
return
|
||||
}
|
||||
reg.waiting.postMessage('skip-waiting')
|
||||
}
|
||||
|
||||
function onUpdateFound (registration) {
|
||||
const newWorker = registration.installing
|
||||
|
||||
newWorker.addEventListener('statechange', async () => {
|
||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||
snackbar.announce('App update available.', 'Reload', () => document.location.reload(true))
|
||||
snackbar.announce('App update available.', 'Reload', async () => {
|
||||
await skipWaiting()
|
||||
document.location.reload(true)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
|
@ -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";
|
|
@ -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";
|
|
@ -35,6 +35,9 @@ self.addEventListener('install', event => {
|
|||
caches.open(WEBPACK_ASSETS).then(cache => cache.addAll(webpackAssets)),
|
||||
caches.open(ASSETS).then(cache => cache.addAll(assets))
|
||||
])
|
||||
// We shouldn't have to do this, but the previous page could be an old one,
|
||||
// which would not send us a postMessage to skipWaiting().
|
||||
// See https://github.com/nolanlawson/pinafore/issues/1243
|
||||
self.skipWaiting()
|
||||
})())
|
||||
})
|
||||
|
@ -243,3 +246,11 @@ self.addEventListener('notificationclick', event => {
|
|||
}
|
||||
})())
|
||||
})
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
switch (event.data) {
|
||||
case 'skip-waiting':
|
||||
self.skipWaiting()
|
||||
break
|
||||
}
|
||||
})
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import {
|
||||
settingsNavButton,
|
||||
homeNavButton,
|
||||
disableInfiniteScroll,
|
||||
scrollToStatus,
|
||||
loadMoreButton, getFirstVisibleStatus, scrollFromStatusToStatus, sleep, getActiveElementAriaPosInSet
|
||||
} from '../utils'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
import { Selector as $ } from 'testcafe'
|
||||
|
||||
fixture`036-disable-infinite-load.js`
|
||||
.page`http://localhost:4002`
|
||||
|
||||
test('Can disable loading items at bottom of timeline', async t => {
|
||||
await loginAsFoobar(t)
|
||||
await t.click(settingsNavButton)
|
||||
.click($('a').withText('General'))
|
||||
.click(disableInfiniteScroll)
|
||||
.expect(disableInfiniteScroll.checked).ok()
|
||||
.click(homeNavButton)
|
||||
.expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('20')
|
||||
await scrollToStatus(t, 20)
|
||||
await t
|
||||
.click(loadMoreButton)
|
||||
.expect(getActiveElementAriaPosInSet()).eql('20')
|
||||
.expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('40')
|
||||
await scrollFromStatusToStatus(t, 20, 40)
|
||||
await t
|
||||
.click(loadMoreButton)
|
||||
.expect(getActiveElementAriaPosInSet()).eql('40')
|
||||
.expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('47')
|
||||
await scrollFromStatusToStatus(t, 40, 47)
|
||||
await t
|
||||
.click(loadMoreButton)
|
||||
await sleep(1000)
|
||||
await t
|
||||
.expect(loadMoreButton.exists).ok()
|
||||
.expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('47')
|
||||
})
|
|
@ -4,7 +4,11 @@ import {
|
|||
instanceSettingNotificationReblogs,
|
||||
notificationBadge,
|
||||
instanceSettingNotificationFavs,
|
||||
instanceSettingNotificationMentions, instanceSettingNotificationFollows
|
||||
instanceSettingNotificationMentions,
|
||||
instanceSettingNotificationFollows,
|
||||
notificationsNavButton,
|
||||
getUrl,
|
||||
sleep, showMoreButton, scrollToBottom, scrollToTop
|
||||
} from '../utils'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
import { Selector as $ } from 'testcafe'
|
||||
|
@ -91,3 +95,33 @@ test('Notification timeline filters correctly affect counts - follows', async t
|
|||
.expect(notificationBadge.innerText).eql('1', { timeout })
|
||||
await unfollowAs('ExternalLinks', 'foobar')
|
||||
})
|
||||
|
||||
test('Notification timeline filters correctly show "show more" button', async t => {
|
||||
await loginAsFoobar(t)
|
||||
await t
|
||||
.click(settingsNavButton)
|
||||
.click($('a').withText('Instances'))
|
||||
.click($('a').withText('localhost:3000'))
|
||||
.click(instanceSettingNotificationMentions)
|
||||
.expect(instanceSettingNotificationMentions.checked).notOk()
|
||||
.click(notificationsNavButton)
|
||||
.expect(getUrl()).contains('/notifications')
|
||||
.expect(getNthStatusContent(1).exists).ok()
|
||||
await scrollToBottom()
|
||||
await sleep(1000)
|
||||
await postAs('admin', 'hey @foobar you should ignore this')
|
||||
await sleep(1000)
|
||||
await scrollToTop()
|
||||
await sleep(1000)
|
||||
await t
|
||||
.expect(showMoreButton.innerText).contains('Show 0 more') // not shown
|
||||
await scrollToBottom()
|
||||
await sleep(1000)
|
||||
await followAs('ExternalLinks', 'foobar')
|
||||
await sleep(1000)
|
||||
await scrollToTop()
|
||||
await sleep(1000)
|
||||
await t
|
||||
.expect(showMoreButton.innerText).contains('Show 1 more', { timeout: 20000 })
|
||||
await unfollowAs('ExternalLinks', 'foobar')
|
||||
})
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import {
|
||||
settingsNavButton,
|
||||
homeNavButton,
|
||||
disableInfiniteScroll,
|
||||
getFirstVisibleStatus,
|
||||
getUrl,
|
||||
showMoreButton, getNthStatusContent
|
||||
} from '../utils'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
import { Selector as $ } from 'testcafe'
|
||||
import { postAs } from '../serverActions'
|
||||
|
||||
fixture`128-disable-infinite-load.js`
|
||||
.page`http://localhost:4002`
|
||||
|
||||
test('Can disable loading items at top of timeline', async t => {
|
||||
await loginAsFoobar(t)
|
||||
await t.click(settingsNavButton)
|
||||
.click($('a').withText('General'))
|
||||
.click(disableInfiniteScroll)
|
||||
.expect(disableInfiniteScroll.checked).ok()
|
||||
.click(homeNavButton)
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
.expect(getFirstVisibleStatus().exists).ok()
|
||||
await postAs('admin', 'hey hey hey this is new')
|
||||
await t
|
||||
.expect(showMoreButton.innerText).contains('Show 1 more', {
|
||||
timeout: 20000
|
||||
})
|
||||
.click(showMoreButton)
|
||||
.expect(getNthStatusContent(1).innerText).contains('hey hey hey this is new')
|
||||
})
|
|
@ -0,0 +1,36 @@
|
|||
import {
|
||||
settingsNavButton,
|
||||
homeNavButton,
|
||||
disableUnreadNotifications,
|
||||
getFirstVisibleStatus,
|
||||
getUrl,
|
||||
notificationsNavButton, getTitleText, sleep
|
||||
} from '../utils'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
import { Selector as $ } from 'testcafe'
|
||||
import { postAs } from '../serverActions'
|
||||
|
||||
fixture`129-wellness.js`
|
||||
.page`http://localhost:4002`
|
||||
|
||||
test('Can disable unread notification counts', async t => {
|
||||
await loginAsFoobar(t)
|
||||
await t.click(settingsNavButton)
|
||||
.click($('a').withText('Wellness'))
|
||||
.click(disableUnreadNotifications)
|
||||
.expect(disableUnreadNotifications.checked).ok()
|
||||
.click(homeNavButton)
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
.expect(getFirstVisibleStatus().exists).ok()
|
||||
await postAs('admin', 'hey @foobar')
|
||||
await sleep(2000)
|
||||
await t
|
||||
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications')
|
||||
.expect(getTitleText()).notContains('(1)')
|
||||
.click(settingsNavButton)
|
||||
.click($('a').withText('Wellness'))
|
||||
.click(disableUnreadNotifications)
|
||||
.expect(disableUnreadNotifications.checked).notOk()
|
||||
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (1 notification)')
|
||||
.expect(getTitleText()).contains('(1)')
|
||||
})
|
|
@ -48,11 +48,15 @@ export const generalSettingsButton = $('a[href="/settings/general"]')
|
|||
export const markMediaSensitiveInput = $('#choice-mark-media-sensitive')
|
||||
export const neverMarkMediaSensitiveInput = $('#choice-never-mark-media-sensitive')
|
||||
export const removeEmojiFromDisplayNamesInput = $('#choice-omit-emoji-in-display-names')
|
||||
export const disableInfiniteScroll = $('#choice-disable-infinite-scroll')
|
||||
export const disableUnreadNotifications = $('#choice-disable-unread-notification-counts')
|
||||
export const dialogOptionsOption = $(`.modal-dialog button`)
|
||||
export const emojiSearchInput = $('.emoji-mart-search input')
|
||||
export const confirmationDialogOKButton = $('.confirmation-dialog-form-flex button:nth-child(1)')
|
||||
export const confirmationDialogCancelButton = $('.confirmation-dialog-form-flex button:nth-child(2)')
|
||||
|
||||
export const loadMoreButton = $('.loading-footer button')
|
||||
|
||||
export const composeModalInput = $('.modal-dialog .compose-box-input')
|
||||
export const composeModalComposeButton = $('.modal-dialog .compose-box-button')
|
||||
export const composeModalContentWarningInput = $('.modal-dialog .content-warning-input')
|
||||
|
@ -119,6 +123,10 @@ export const getActiveElementRectTop = exec(() => (
|
|||
(document.activeElement && document.activeElement.getBoundingClientRect().top) || -1
|
||||
))
|
||||
|
||||
export const getActiveElementAriaPosInSet = exec(() => (
|
||||
(document.activeElement && document.activeElement.getAttribute('aria-posinset')) || ''
|
||||
))
|
||||
|
||||
export const getActiveElementInsideNthStatus = exec(() => {
|
||||
let element = document.activeElement
|
||||
while (element) {
|
||||
|
@ -428,16 +436,20 @@ export async function validateTimeline (t, timeline) {
|
|||
}
|
||||
|
||||
export async function scrollToStatus (t, n) {
|
||||
return scrollFromStatusToStatus(t, 1, n)
|
||||
}
|
||||
|
||||
export async function scrollFromStatusToStatus (t, start, end) {
|
||||
let timeout = 20000
|
||||
for (let i = 1; i < n; i++) {
|
||||
for (let i = start; i < end; i++) {
|
||||
await t.expect(getNthStatus(i).exists).ok({ timeout })
|
||||
.hover(getNthStatus(i))
|
||||
.expect($('.loading-footer').exist).notOk({ timeout })
|
||||
.expect($(`${getNthStatusSelector(i)} .status-toolbar`).exists).ok({ timeout })
|
||||
.hover($(`${getNthStatusSelector(i)} .status-toolbar`))
|
||||
.expect($('.loading-footer').exist).notOk({ timeout })
|
||||
}
|
||||
await t.hover(getNthStatus(n))
|
||||
await t
|
||||
.expect(getNthStatus(end).exists).ok({ timeout })
|
||||
.hover(getNthStatus(end))
|
||||
}
|
||||
|
||||
export async function clickToNotificationsAndBackHome (t) {
|
||||
|
|
Loading…
Reference in New Issue