Compare commits

..

5 Commits

91 changed files with 1081 additions and 3172 deletions

View File

@ -11,6 +11,7 @@ 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')
@ -21,9 +22,11 @@ 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

@ -42,15 +42,9 @@ module.exports = [
{ id: 'fa-circle-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/circle-o.svg' },
{ id: 'fa-angle-left', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-left.svg' },
{ id: 'fa-angle-right', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-right.svg' },
{ id: 'fa-angle-down', src: 'src/thirdparty/font-awesome-svg-png/white/svg/angle-down.svg' },
{ id: 'fa-search-minus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search-minus.svg' },
{ id: 'fa-search-plus', src: 'src/thirdparty/font-awesome-svg-png/white/svg/search-plus.svg' },
{ id: 'fa-share-square-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/share-square-o.svg' },
{ id: 'fa-flag', src: 'src/thirdparty/font-awesome-svg-png/white/svg/flag.svg' },
{ id: 'fa-suitcase', src: 'src/thirdparty/font-awesome-svg-png/white/svg/suitcase.svg' },
{ 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-info-circle', src: 'src/thirdparty/font-awesome-svg-png/white/svg/info-circle.svg' }
{ id: 'fa-suitcase', src: 'src/thirdparty/font-awesome-svg-png/white/svg/suitcase.svg' }
]

View File

@ -1,7 +1,7 @@
{
"name": "pinafore",
"description": "Alternative web client for Mastodon",
"version": "1.9.0",
"version": "1.7.0",
"scripts": {
"lint": "standard && standard --plugin html 'src/routes/**/*.html'",
"lint-fix": "standard --fix && standard --fix --plugin html 'src/routes/**/*.html'",
@ -43,10 +43,10 @@
"build-now-json": "node -r esm ./bin/build-now-json.js"
},
"dependencies": {
"@babel/core": "^7.4.5",
"@babel/core": "^7.4.4",
"@gamestdio/websocket": "^0.3.2",
"@webcomponents/custom-elements": "^1.2.4",
"babel-loader": "^8.0.6",
"babel-loader": "^8.0.5",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"cheerio": "^1.0.0-rc.2",
"child-process-promise": "^2.2.1",
@ -61,9 +61,9 @@
"emoji-regex": "^8.0.0",
"encoding": "^0.1.12",
"escape-html": "^1.0.3",
"esm": "^3.2.25",
"esm": "^3.2.22",
"events-light": "^1.0.5",
"express": "^4.17.1",
"express": "^4.16.4",
"file-api": "^0.10.4",
"file-drop-element": "0.2.0",
"form-data": "^2.3.3",
@ -74,7 +74,7 @@
"lodash-es": "^4.17.11",
"lodash-webpack-plugin": "^0.11.5",
"mkdirp": "^0.5.1",
"node-fetch": "^2.6.0",
"node-fetch": "^2.5.0",
"node-sass": "^4.12.0",
"npm-run-all": "^4.1.5",
"p-any": "^2.1.0",
@ -86,28 +86,28 @@
"quick-lru": "^4.0.0",
"remount": "^0.11.0",
"requestidlecallback": "^0.3.0",
"rollup": "^1.12.4",
"rollup": "^1.11.3",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^5.0.0",
"rollup-plugin-terser": "^4.0.4",
"sapper": "nolanlawson/sapper#for-pinafore-14",
"stringz": "^2.0.0",
"stringz": "^1.0.0",
"svelte": "^2.16.1",
"svelte-extras": "^2.0.2",
"svelte-loader": "^2.13.4",
"svelte-loader": "^2.13.3",
"svelte-transitions": "^1.2.0",
"svgo": "^1.2.2",
"terser-webpack-plugin": "^1.3.0",
"terser-webpack-plugin": "^1.2.3",
"text-encoding": "^0.7.0",
"tiny-queue": "^0.2.1",
"webpack": "^4.32.2",
"webpack": "^4.31.0",
"webpack-bundle-analyzer": "^3.3.2"
},
"devDependencies": {
"assert": "^2.0.0",
"eslint-plugin-html": "^5.0.5",
"assert": "^1.5.0",
"eslint-plugin-html": "^5.0.3",
"fake-indexeddb": "^2.1.0",
"mocha": "^6.1.4",
"now": "^15.3.0",
"now": "^15.2.0",
"standard": "^12.0.1",
"testcafe": "^1.1.4"
},

View File

@ -10,24 +10,14 @@
<link id='theManifest' rel='manifest' href='/manifest.json' >
<link id='theFavicon' rel='icon' type='image/png' href='/favicon.png' >
<link rel="apple-touch-icon" href="/apple-icon.png" >
<link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120.png" >
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180.png" >
<meta name="mobile-web-app-capable" content="yes" >
<meta name="apple-mobile-web-app-title" content="Pinafore" >
<meta name="apple-mobile-web-app-status-bar-style" content="white" >
<!-- 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 {

View File

@ -3,21 +3,16 @@
// To allow CSP to work correctly, we also calculate a sha256 hash during
// the build process and write it to checksum.js.
import { INLINE_THEME, DEFAULT_THEME, switchToTheme } from '../routes/_utils/themeEngine'
import { testHasLocalStorageOnce } from '../routes/_utils/testStorage'
import { DEFAULT_LIGHT_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 {
currentInstance,
instanceThemes,
disableCustomScrollbars,
enableGrayscale
} = storeLite.get()
const theme = (instanceThemes && instanceThemes[currentInstance]) || DEFAULT_THEME
const safeParse = str => (typeof str === 'undefined' || str === 'undefined') ? undefined : JSON.parse(str)
const hasLocalStorage = testHasLocalStorageOnce()
const currentInstance = hasLocalStorage && safeParse(localStorage.store_currentInstance)
if (currentInstance) {
// Do prefetch if we're logged in, so we can connect faster to the other origin.
@ -31,24 +26,24 @@ if (currentInstance) {
document.head.appendChild(link)
}
if (theme !== INLINE_THEME) {
let theme = (currentInstance &&
localStorage.store_instanceThemes &&
safeParse(localStorage.store_instanceThemes)[safeParse(localStorage.store_currentInstance)]) ||
DEFAULT_THEME
if (theme !== DEFAULT_LIGHT_THEME) {
// switch theme ASAP to minimize flash of default theme
switchToTheme(theme, enableGrayscale)
switchToTheme(theme)
}
if (enableGrayscale) {
document.getElementById('theGrayscaleStyle')
.setAttribute('media', 'all') // enables the style
}
if (!currentInstance) {
if (!hasLocalStorage || !currentInstance) {
// if not logged in, show all these 'hidden-from-ssr' elements
onUserIsLoggedOut()
}
if (disableCustomScrollbars) {
document.getElementById('theScrollbarStyle')
.setAttribute('media', 'only x') // disables the style
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
}
// hack to make the scrollbars rounded only on macOS

View File

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

View File

@ -22,7 +22,7 @@ export async function insertHandleForReply (statusId) {
export async function postStatus (realm, text, inReplyToId, mediaIds,
sensitive, spoilerText, visibility,
mediaDescriptions, inReplyToUuid, poll) {
mediaDescriptions, inReplyToUuid) {
let { currentInstance, accessToken, online } = store.get()
if (!online) {
@ -41,7 +41,7 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
return description && putMediaDescription(currentInstance, accessToken, mediaIds[i], description)
}))
let status = await postStatusToServer(currentInstance, accessToken, text,
inReplyToId, mediaIds, sensitive, spoilerText, visibility, poll)
inReplyToId, mediaIds, sensitive, spoilerText, visibility)
addStatusOrNotification(currentInstance, 'home', status)
store.clearComposeData(realm)
emit('postedStatus', realm, inReplyToUuid)

View File

@ -1,18 +0,0 @@
import { store } from '../_store/store'
export function enablePoll (realm) {
store.setComposeData(realm, {
poll: {
options: [
'',
''
]
}
})
}
export function disablePoll (realm) {
store.setComposeData(realm, {
poll: null
})
}

View File

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

View File

@ -1,25 +0,0 @@
import { getPoll as getPollApi, voteOnPoll as voteOnPollApi } from '../_api/polls'
import { store } from '../_store/store'
import { toast } from '../_components/toast/toast'
export async function getPoll (pollId) {
let { currentInstance, accessToken } = store.get()
try {
let poll = await getPollApi(currentInstance, accessToken, pollId)
return poll
} catch (e) {
console.error(e)
toast.say('Unable to refresh poll: ' + (e.message || ''))
}
}
export async function voteOnPoll (pollId, choices) {
let { currentInstance, accessToken } = store.get()
try {
let poll = await voteOnPollApi(currentInstance, accessToken, pollId, choices.map(_ => _.toString()))
return poll
} catch (e) {
console.error(e)
toast.say('Unable to vote in poll: ' + (e.message || ''))
}
}

View File

@ -114,7 +114,7 @@ export async function setupTimeline () {
stop('setupTimeline')
}
export async function fetchMoreItemsAtBottomOfTimeline (instanceName, timelineName) {
export async function fetchTimelineItemsOnScrollToBottom (instanceName, timelineName) {
console.log('setting runningUpdate: true')
store.setForTimeline(instanceName, timelineName, { runningUpdate: true })
await fetchTimelineItemsAndPossiblyFallBack()

View File

@ -1,12 +0,0 @@
import { get, post, DEFAULT_TIMEOUT, WRITE_TIMEOUT } from '../_utils/ajax'
import { auth, basename } from './utils'
export async function getPoll (instanceName, accessToken, pollId) {
let url = `${basename(instanceName)}/api/v1/polls/${pollId}`
return get(url, auth(accessToken), { timeout: DEFAULT_TIMEOUT })
}
export async function voteOnPoll (instanceName, accessToken, pollId, choices) {
let url = `${basename(instanceName)}/api/v1/polls/${pollId}/votes`
return post(url, { choices }, auth(accessToken), { timeout: WRITE_TIMEOUT })
}

View File

@ -2,7 +2,7 @@ import { auth, basename } from './utils'
import { DEFAULT_TIMEOUT, get, post, WRITE_TIMEOUT } from '../_utils/ajax'
export async function postStatus (instanceName, accessToken, text, inReplyToId, mediaIds,
sensitive, spoilerText, visibility, poll) {
sensitive, spoilerText, visibility) {
let url = `${basename(instanceName)}/api/v1/statuses`
let body = {
@ -11,8 +11,7 @@ export async function postStatus (instanceName, accessToken, text, inReplyToId,
media_ids: mediaIds,
sensitive: sensitive,
spoiler_text: spoilerText,
visibility: visibility,
poll: poll
visibility: visibility
}
for (let key of Object.keys(body)) {

View File

@ -1,76 +0,0 @@
<div class="select-wrapper {className || ''}">
<select on:change aria-label={label}>
{#each options as option (option.value)}
<option value="{option.value}" selected="{option.value === defaultValue ? 'selected' : ''}">
{option.label}
</option>
{/each}
</select>
<div class="select-dropdown-icon-wrapper">
<SvgIcon href="#fa-angle-down" className="select-dropdown-icon"/>
</div>
</div>
<style>
.select-wrapper {
position: relative;
display: inline-block;
}
.select-dropdown-icon-wrapper {
position: absolute;
right: 15px;
top: 0;
bottom: 0;
display: flex;
align-items: center;
pointer-events: none;
}
:global(.select-dropdown-icon) {
width: 18px;
height: 18px;
min-width: 18px;
fill: var(--action-button-deemphasized-fill-color);
}
select {
display: inline-block;
padding: 5px 35px 5px 15px;
margin: 0;
font-size: 1.3em;
color: var(--body-text-color);
line-height: 1.1;
box-sizing: border-box;
border: 1px solid var(--main-border);
border-radius: 10px;
-moz-appearance: none;
-webkit-appearance: none;
background-color: var(--input-bg);
cursor: pointer;
}
select:hover {
background-color: var(--button-bg-hover);
}
select:active {
background-color: var(--button-bg-active);
}
select::-ms-expand {
display: none;
}
select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 var(--body-text-color);
}
select option {
font-weight:normal;
}
</style>
<script>
import SvgIcon from './SvgIcon.html'
export default {
data: () => ({
defaultValue: '',
className: ''
}),
components: {
SvgIcon
}
}
</script>

View File

@ -7,7 +7,7 @@
{#if hidePage}
<LoadingPage />
{/if}
<LazyComposeBox realm="home" hidden={hidePage}/>
<ComposeBox 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 LazyComposeBox from './compose/LazyComposeBox.html'
import ComposeBox from './compose/ComposeBox.html'
export default {
oncreate () {
@ -44,9 +44,9 @@
},
store: () => store,
components: {
LazyComposeBox,
LazyTimeline,
LoadingPage
LoadingPage,
ComposeBox
}
}
</script>

View File

@ -13,13 +13,7 @@
<ComposeInput {realm} {text} {autoFocus} on:postAction="doPostStatus()" />
<ComposeLengthGauge {length} {overLimit} />
<ComposeAutosuggest {realm} {text} />
{#if poll && poll.options && poll.options.length}
<div class="compose-poll-wrapper"
transition:slide="{duration: 333}">
<ComposePoll {realm} {poll} />
</div>
{/if}
<ComposeToolbar {realm} {postPrivacy} {media} {contentWarningShown} {text} {poll} />
<ComposeToolbar {realm} {postPrivacy} {media} {contentWarningShown} {text} />
<ComposeLengthIndicator {length} {overLimit} />
<ComposeMedia {realm} {media} />
</div>
@ -44,7 +38,6 @@
"avatar input input input"
"avatar gauge gauge gauge"
"avatar autosuggest autosuggest autosuggest"
"avatar poll poll poll"
"avatar toolbar toolbar length"
"avatar media media media";
grid-template-columns: min-content minmax(0, max-content) 1fr 1fr;
@ -69,10 +62,6 @@
grid-area: cw;
}
.compose-poll-wrapper {
grid-area: poll;
}
@media (max-width: 767px) {
.compose-box {
padding: 10px 10px 0 10px;
@ -94,14 +83,12 @@
import ComposeContentWarning from './ComposeContentWarning.html'
import ComposeFileDrop from './ComposeFileDrop.html'
import ComposeAutosuggest from './ComposeAutosuggest.html'
import ComposePoll from './ComposePoll.html'
import { measureText } from '../../_utils/measureText'
import { POST_PRIVACY_OPTIONS } from '../../_static/statuses'
import { store } from '../../_store/store'
import { slide } from '../../_transitions/slide'
import { slide } from 'svelte-transitions'
import { postStatus, insertHandleForReply, setReplySpoiler, setReplyVisibility } from '../../_actions/compose'
import { classname } from '../../_utils/classname'
import { POLL_EXPIRY_DEFAULT } from '../../_static/polls'
export default {
oncreate () {
@ -131,8 +118,7 @@
ComposeMedia,
ComposeContentWarning,
ComposeFileDrop,
ComposeAutosuggest,
ComposePoll
ComposeAutosuggest
},
data: () => ({
size: void 0,
@ -158,7 +144,6 @@
composeData: ({ $currentComposeData, realm }) => $currentComposeData[realm] || {},
text: ({ composeData }) => composeData.text || '',
media: ({ composeData }) => composeData.media || [],
poll: ({ composeData }) => composeData.poll,
inReplyToId: ({ composeData }) => composeData.inReplyToId,
postPrivacy: ({ postPrivacyKey }) => POST_PRIVACY_OPTIONS.find(_ => _.key === postPrivacyKey),
defaultPostPrivacyKey: ({ $currentVerifyCredentials }) => (
@ -187,8 +172,7 @@
realm,
overLimit,
inReplyToUuid, // typical replies, using Pinafore-specific uuid
inReplyToId, // delete-and-redraft replies, using standard id
poll
inReplyToId // delete-and-redraft replies, using standard id
} = this.get()
let sensitive = media.length && !!contentWarning
let mediaIds = media.map(_ => _.data.id)
@ -199,25 +183,10 @@
return // do nothing if invalid
}
let hasPoll = poll && poll.options && poll.options.length
if (hasPoll) {
// validate poll
if (poll.options.length < 2 || !poll.options.every(Boolean)) {
return
}
}
// convert internal poll format to the format Mastodon's REST API uses
let pollToPost = hasPoll && {
expires_in: (poll.expiry || POLL_EXPIRY_DEFAULT).toString(),
multiple: !!poll.multiple,
options: poll.options
}
/* no await */
postStatus(realm, text, inReplyTo, mediaIds,
sensitive, contentWarning, postPrivacyKey,
mediaDescriptions, inReplyToUuid, pollToPost)
mediaDescriptions, inReplyToUuid)
}
}
}

View File

@ -6,7 +6,7 @@
.compose-box-length {
grid-area: length;
justify-self: right;
color: var(--length-indicator-color);
color: var(--main-theme-color);
font-size: 1.3em;
align-self: center;
}
@ -53,4 +53,4 @@
observe
}
}
</script>
</script>

View File

@ -1,22 +1,18 @@
{#if media.length}
<ul class="compose-media-container"
aria-label="Media uploads"
style="grid-template-columns: repeat({media.length}, 1fr);"
>
<div class="compose-media-container" style="grid-template-columns: repeat({media.length}, 1fr);">
{#each media as mediaItem, index}
<ComposeMediaItem {realm} {mediaItem} {index} {media} />
{/each}
</ul>
</div>
{/if}
<style>
.compose-media-container {
grid-area: media;
list-style: none;
display: grid;
grid-column-gap: 5px;
align-items: center;
justify-content: center;
margin: 10px 0 0 0;
margin-top: 10px;
background: var(--form-bg);
padding: 5px;
border-radius: 4px;

View File

@ -1,4 +1,4 @@
<li class="compose-media compose-media-realm-{realm}">
<div class="compose-media compose-media-realm-{realm}">
<img src={mediaItem.data.preview_url} {alt} />
<div class="compose-media-delete">
<button class="compose-media-delete-button"
@ -8,21 +8,19 @@
</button>
</div>
<div class="compose-media-alt">
<textarea id="compose-media-input-{uuid}"
<input id="compose-media-input-{uuid}"
type="text"
class="compose-media-alt-input"
placeholder="Describe for the visually impaired"
ref:textarea
placeholder="Description"
bind:value=rawText
></textarea>
>
<label for="compose-media-input-{uuid}" class="sr-only">
Describe {shortName} for the visually impaired
</label>
</div>
</li>
</div>
<style>
.compose-media {
margin: 0;
padding: 0;
height: 200px;
overflow: hidden;
flex-direction: column;
@ -51,9 +49,6 @@
font-size: 1.2em;
background: var(--alt-input-bg);
color: var(--body-text-color);
max-height: 100px;
border: 1px solid var(--input-border);
resize: none;
}
.compose-media-alt-input:focus {
background: var(--main-bg);
@ -69,15 +64,12 @@
margin: 2px;
}
.compose-media-delete-button {
padding: 7px 10px 5px;
background: var(--floating-button-bg);
border: 1px solid var(--button-border);
padding: 10px;
background: none;
border: none;
}
.compose-media-delete-button:hover {
background: var(--floating-button-bg-hover);
}
.compose-media-delete-button:active {
background: var(--floating-button-bg-active);
background: var(--toast-border);
}
:global(.compose-media-delete-button-svg) {
fill: var(--button-text);
@ -93,9 +85,6 @@
.compose-media-realm-dialog {
max-height: 15vh;
}
.compose-media-alt-input {
max-height: 7vh;
}
}
</style>
<script>
@ -105,16 +94,11 @@
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
import { observe } from 'svelte-extras'
import SvgIcon from '../SvgIcon.html'
import { autosize } from '../../_thirdparty/autosize/autosize'
export default {
oncreate () {
this.setupSyncFromStore()
this.setupSyncToStore()
this.setupAutosize()
},
ondestroy () {
this.teardownAutosize()
},
data: () => ({
rawText: ''
@ -155,12 +139,6 @@
saveStore()
}, { init: false })
},
setupAutosize () {
autosize(this.refs.textarea)
},
teardownAutosize () {
autosize.destroy(this.refs.textarea)
},
onDeleteMedia () {
let {
realm,

View File

@ -1,157 +0,0 @@
<section class="compose-poll" aria-label="Create poll">
{#each poll.options as option, i}
<input id="poll-option-{realm}-{i}"
type="text"
maxlength="25"
on:change="onChange(i)"
placeholder="Choice {i + 1}"
>
<IconButton
label="Remove choice {i + 1}"
href="#fa-times"
muted={true}
on:click="onDeleteClick(i)"
/>
{/each}
<div>
<input type="checkbox"
id="poll-option-multiple-{realm}"
on:change="onMultipleChange()"
>
<label class="multiple-choice-label"
for="poll-option-multiple-{realm}">
Multiple choice
</label>
<Select className="poll-expiry-select"
options={pollExpiryOptions}
defaultValue={pollExpiryDefaultValue}
on:change="onExpiryChange(event)"
label="Poll duration"
/>
</div>
<IconButton
className="add-poll-choice-button"
label="Add choice"
href="#fa-plus"
muted={true}
disabled={poll.options.length === 4}
on:click="onAddClick()"
/>
{#each poll.options as option, i}
<label id="poll-option-label-{realm}-{i}"
class="sr-only"
for="poll-option-{realm}-{i}">
Choice {i + 1}
</label>
{/each}
</section>
<style>
.compose-poll {
margin: 10px 0 10px 5px;
display: grid;
grid-template-columns: minmax(0, max-content) max-content;
grid-row-gap: 10px;
align-items: center;
}
:global(.poll-expiry-select) {
margin-left: 10px;
}
.multiple-choice-label {
margin-left: 5px;
}
@media (max-width: 767px) {
:global(.poll-expiry-select) {
display: block;
margin-left: 0;
margin-top: 10px;
}
:global(.add-poll-choice-button) {
align-self: flex-start;
}
}
</style>
<script>
import IconButton from '../IconButton.html'
import Select from '../Select.html'
import { store } from '../../_store/store'
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
import { POLL_EXPIRY_DEFAULT, POLL_EXPIRY_OPTIONS } from '../../_static/polls'
function flushPollOptionsToDom (poll, realm) {
for (let i = 0; i < poll.options.length; i++) {
let element = document.getElementById(`poll-option-${realm}-${i}`)
element.value = poll.options[i]
}
}
export default {
oncreate () {
let { realm } = this.get()
let poll = this.store.getComposeData(realm, 'poll')
flushPollOptionsToDom(poll, realm)
document.getElementById(`poll-option-multiple-${realm}`).checked = !!poll.multiple
this.set({ pollExpiryDefaultValue: poll.expiry || POLL_EXPIRY_DEFAULT })
},
data: () => ({
pollExpiryOptions: POLL_EXPIRY_OPTIONS,
pollExpiryDefaultValue: POLL_EXPIRY_DEFAULT
}),
store: () => store,
methods: {
onChange (i) {
scheduleIdleTask(() => {
let { realm } = this.get()
let element = document.getElementById(`poll-option-${realm}-${i}`)
let poll = this.store.getComposeData(realm, 'poll')
poll.options[i] = element.value
this.store.setComposeData(realm, { poll })
})
},
onMultipleChange () {
requestAnimationFrame(() => {
let { realm } = this.get()
let element = document.getElementById(`poll-option-multiple-${realm}`)
let poll = this.store.getComposeData(realm, 'poll')
poll.multiple = !!element.checked
this.store.setComposeData(realm, { poll })
})
},
onDeleteClick (i) {
requestAnimationFrame(() => {
let { realm } = this.get()
let poll = this.store.getComposeData(realm, 'poll')
poll.options.splice(i, 1)
this.store.setComposeData(realm, { poll })
flushPollOptionsToDom(poll, realm)
})
},
onAddClick () {
requestAnimationFrame(() => {
let { realm } = this.get()
let poll = this.store.getComposeData(realm, 'poll')
if (!poll.options.length !== 4) {
poll.options.push('')
}
this.store.setComposeData(realm, { poll })
})
},
onExpiryChange (e) {
requestAnimationFrame(() => {
let { realm } = this.get()
let { value } = e.target
let poll = this.store.getComposeData(realm, 'poll')
poll.expiry = parseInt(value, 10)
this.store.setComposeData(realm, { poll })
})
}
},
components: {
IconButton,
Select
}
}
</script>

View File

@ -1,13 +1,11 @@
<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'}
@ -15,21 +13,11 @@
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()"
pressable="true"
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()"
@ -52,13 +40,6 @@
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'
@ -67,7 +48,6 @@
import { doMediaUpload } from '../../_actions/media'
import { toggleContentWarningShown } from '../../_actions/contentWarnings'
import { mediaAccept } from '../../_static/media'
import { enablePoll, disablePoll } from '../../_actions/composePoll'
export default {
components: {
@ -99,14 +79,6 @@
onContentWarningClick () {
let { realm } = this.get()
toggleContentWarningShown(realm)
},
onPollClick () {
let { poll, realm } = this.get()
if (poll && poll.options && poll.options.length) {
disablePoll(realm)
} else {
enablePoll(realm)
}
}
}
}

View File

@ -1,16 +0,0 @@
{#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>

View File

@ -29,4 +29,4 @@
}
}
}
</script>
</script>

View File

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

View File

@ -1,48 +0,0 @@
<div class="generic-instance-settings">
<form aria-label={label} ref:form>
{#each options as option, i (option.key) }
{#if i > 0}
<br>
{/if}
<input type="checkbox"
id="instance-option-{option.key}"
name="{option.key}"
on:change="onChange(event)"
>
<label for="instance-option-{option.key}">
{option.label}
</label>
{/each}
</form>
</div>
<style>
.generic-instance-settings {
background: var(--form-bg);
border: 1px solid var(--main-border);
border-radius: 4px;
display: block;
padding: 20px;
line-height: 2em;
}
</style>
<script>
import { store } from '../../../_store/store'
export default {
oncreate () {
let { instanceName, options } = this.get()
let { form } = this.refs
for (let { key, defaultValue } of options) {
form.elements[key].checked = this.store.getInstanceSetting(instanceName, key, defaultValue)
}
},
methods: {
onChange (event) {
let { instanceName } = this.get()
let { target } = event
this.store.setInstanceSetting(instanceName, target.name, target.checked)
}
},
store: () => store
}
</script>

View File

@ -1,29 +0,0 @@
<GenericInstanceSettings
{instanceName}
{options}
label="Home timeline filter settings"
/>
<script>
import GenericInstanceSettings from './GenericInstanceSettings.html'
import { HOME_REBLOGS, HOME_REPLIES } from '../../../_static/instanceSettings'
export default {
data: () => ({
options: [
{
key: HOME_REBLOGS,
label: 'Show boosts',
defaultValue: true
},
{
key: HOME_REPLIES,
label: 'Show replies',
defaultValue: true
}
]
}),
components: {
GenericInstanceSettings
}
}
</script>

View File

@ -1,50 +0,0 @@
<GenericInstanceSettings
{instanceName}
{options}
label="Notification filter settings"
/>
<script>
import GenericInstanceSettings from './GenericInstanceSettings.html'
import {
NOTIFICATION_REBLOGS,
NOTIFICATION_FAVORITES,
NOTIFICATION_FOLLOWS,
NOTIFICATION_MENTIONS,
NOTIFICATION_POLLS
} from '../../../_static/instanceSettings'
export default {
data: () => ({
options: [
{
key: NOTIFICATION_FOLLOWS,
label: 'New followers',
defaultValue: true
},
{
key: NOTIFICATION_FAVORITES,
label: 'Favorites',
defaultValue: true
},
{
key: NOTIFICATION_REBLOGS,
label: 'Boosts',
defaultValue: true
},
{
key: NOTIFICATION_MENTIONS,
label: 'Mentions',
defaultValue: true
},
{
key: NOTIFICATION_POLLS,
label: 'Poll results',
defaultValue: true
}
]
}),
components: {
GenericInstanceSettings
}
}
</script>

View File

@ -4,21 +4,18 @@
{:elseif $notificationPermission === "denied"}
<p role="alert">You have denied permission to show notifications.</p>
{/if}
<form id="push-notification-settings"
disabled="{!pushNotificationsSupport}"
ref:form
aria-label="Push notification settings">
{#each options as option, i (option.key)}
{#if i > 0}
<br>
{/if}
<input type="checkbox"
id="push-notifications-{option.key}"
name="{option.key}"
disabled="{!pushNotificationsSupport}"
on:change="onPushSettingsChange(event)">
<label for="push-notifications-{option.key}">{option.label}</label>
{/each}
<form id="push-notification-settings" disabled="{!pushNotificationsSupport}" ref:pushNotificationsForm aria-label="Push notification settings">
<input type="checkbox" id="push-notifications-follow" name="follow" disabled="{!pushNotificationsSupport}" on:change="onPushSettingsChange(event)">
<label for="push-notifications-follow">New followers</label>
<br>
<input type="checkbox" id="push-notifications-favourite" name="favourite" disabled="{!pushNotificationsSupport}" on:change="onPushSettingsChange(event)">
<label for="push-notifications-favourite">Favourites</label>
<br>
<input type="checkbox" id="push-notifications-reblog" name="reblog" disabled="{!pushNotificationsSupport}" on:change="onPushSettingsChange(event)">
<label for="push-notifications-reblog">Boosts</label>
<br>
<input type="checkbox" id="push-notifications-mention" name="mention" disabled="{!pushNotificationsSupport}" on:change="onPushSettingsChange(event)">
<label for="push-notifications-mention">Mentions</label>
</form>
</div>
<style>
@ -30,11 +27,11 @@
padding: 20px;
line-height: 2em;
}
form[disabled="true"] {
.push-notifications form[disabled="true"] {
opacity: 0.5;
}
p {
margin: 0 0 10px 0;
.push-notifications p {
margin: 0;
}
</style>
<script>
@ -43,56 +40,33 @@
import { logOutOfInstance } from '../../../_actions/instances'
import { updatePushSubscriptionForInstance, updateAlerts } from '../../../_actions/pushSubscription'
import { toast } from '../../toast/toast'
import { get } from '../../../_utils/lodash-lite'
export default {
async oncreate () {
let { instanceName, options } = this.get()
let { instanceName } = this.get()
await updatePushSubscriptionForInstance(instanceName)
const { form } = this.refs
const form = this.refs.pushNotificationsForm
const { pushSubscription } = this.store.get()
for (let { key } of options) {
form.elements[key].checked = get(pushSubscription, ['alerts', key])
}
form.elements.follow.checked = pushSubscription && pushSubscription.alerts && pushSubscription.alerts.follow
form.elements.favourite.checked = pushSubscription && pushSubscription.alerts && pushSubscription.alerts.favourite
form.elements.reblog.checked = pushSubscription && pushSubscription.alerts && pushSubscription.alerts.reblog
form.elements.mention.checked = pushSubscription && pushSubscription.alerts && pushSubscription.alerts.mention
},
store: () => store,
data: () => ({
options: [
{
key: 'follow',
label: 'New Followers'
},
{
key: 'favourite',
label: 'Favorites'
},
{
key: 'reblog',
label: 'Boosts'
},
{
key: 'mention',
label: 'Mentions'
},
{
key: 'poll',
label: 'Poll results'
}
]
}),
computed: {
pushNotificationsSupport: ({ $pushNotificationsSupport }) => $pushNotificationsSupport
},
methods: {
async onPushSettingsChange (e) {
const { instanceName, options } = this.get()
const { form } = this.refs
const alerts = {}
for (let { key } of options) {
alerts[key] = form.elements[key].checked
const { instanceName } = this.get()
const form = this.refs.pushNotificationsForm
const alerts = {
follow: form.elements.follow.checked,
favourite: form.elements.favourite.checked,
reblog: form.elements.reblog.checked,
mention: form.elements.mention.checked
}
try {

View File

@ -13,11 +13,11 @@
<StatusAuthorName {...params} />
<StatusAuthorHandle {...params} />
{#if !isStatusInOwnThread}
<StatusRelativeDate {...params} {...timestampParams} />
<StatusRelativeDate {...params} />
{/if}
<StatusSidebar {...params} />
{#if spoilerText}
<StatusSpoiler {...params} {spoilerShown} on:recalculateHeight />
<StatusSpoiler {...params} on:recalculateHeight />
{/if}
{#if !showContent}
<StatusMentions {...params} />
@ -31,13 +31,10 @@
{#if showMedia }
<StatusMediaAttachments {...params} on:recalculateHeight />
{/if}
{#if showPoll}
<StatusPoll {...params} />
{/if}
{#if isStatusInOwnThread}
<StatusDetails {...params} {...timestampParams} />
<StatusDetails {...params} />
{/if}
<StatusToolbar {...params} {replyShown} on:recalculateHeight />
<StatusToolbar {...params} on:recalculateHeight />
{#if replyShown}
<StatusComposeBox {...params} on:recalculateHeight />
{/if}
@ -61,7 +58,6 @@
"sidebar content content content"
"sidebar card card card"
"sidebar media-grp media-grp media-grp"
"sidebar poll poll poll"
"media media media media"
"....... toolbar toolbar toolbar"
"compose compose compose compose";
@ -96,7 +92,6 @@
"card card"
"media-grp media-grp"
"media media"
"poll poll"
"details details"
"toolbar toolbar"
"compose compose";
@ -124,7 +119,6 @@
import StatusSpoiler from './StatusSpoiler.html'
import StatusComposeBox from './StatusComposeBox.html'
import StatusMentions from './StatusMentions.html'
import StatusPoll from './StatusPoll.html'
import Shortcut from '../shortcut/Shortcut.html'
import { store } from '../../_store/store'
import { goto } from '../../../../__sapper__/client'
@ -144,7 +138,7 @@
import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid'
import { statusHtmlToPlainText } from '../../_utils/statusHtmlToPlainText'
const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea', 'label'])
const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea'])
const isUserInputElement = node => INPUT_TAGS.has(node.localName)
const isToolbar = node => node.classList.contains('status-toolbar')
const isStatusArticle = node => node.classList.contains('status-article')
@ -176,7 +170,6 @@
StatusMediaAttachments,
StatusContent,
StatusCard,
StatusPoll,
StatusSpoiler,
StatusComposeBox,
StatusMentions,
@ -268,24 +261,19 @@
originalStatus.card &&
originalStatus.card.title
),
showPoll: ({ originalStatus }) => (
originalStatus.poll
),
showMedia: ({ originalStatus, isStatusInNotification }) => (
!isStatusInNotification &&
originalStatus.media_attachments &&
originalStatus.media_attachments.length
),
originalAccountEmojis: ({ originalAccount }) => (originalAccount.emojis || []),
originalStatusEmojis: ({ originalStatus }) => (originalStatus.emojis || []),
originalAccountDisplayName: ({ originalAccount }) => (originalAccount.display_name || originalAccount.username),
originalAccountAccessibleName: ({ originalAccount, $omitEmojiInDisplayNames }) => {
return getAccountAccessibleName(originalAccount, $omitEmojiInDisplayNames)
},
createdAtDate: ({ originalStatus }) => originalStatus.created_at,
createdAtDateTS: ({ createdAtDate }) => new Date(createdAtDate).getTime(),
absoluteFormattedDate: ({ createdAtDateTS }) => absoluteDateFormatter.format(createdAtDateTS),
timeagoFormattedDate: ({ createdAtDateTS, $now }) => formatTimeagoDate(createdAtDateTS, $now),
absoluteFormattedDate: ({ createdAtDate }) => absoluteDateFormatter.format(new Date(createdAtDate)),
timeagoFormattedDate: ({ createdAtDate }) => formatTimeagoDate(createdAtDate),
reblog: ({ status }) => status.reblog,
ariaLabel: ({ originalAccount, account, plainTextContent, timeagoFormattedDate, spoilerText,
showContent, reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels }) => (
@ -294,7 +282,7 @@
reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels)
),
showHeader: ({ notification, status, timelineType }) => (
(notification && ['reblog', 'favourite', 'poll'].includes(notification.type)) ||
(notification && (notification.type === 'reblog' || notification.type === 'favourite')) ||
status.reblog ||
timelineType === 'pinned'
),
@ -305,26 +293,15 @@
timelineType !== 'search' && 'status-in-timeline',
isStatusInOwnThread && 'status-in-own-thread',
$underlineLinks && 'underline-links',
!$disableTapOnStatus && !isStatusInOwnThread && 'tap-on-status'
!$disableTapOnStatus && 'tap-on-status'
)),
content: ({ originalStatus }) => originalStatus.content || '',
showContent: ({ spoilerText, spoilerShown }) => !spoilerText || spoilerShown,
// These timestamp params may change every 10 seconds due to now() polling, so keep them
// separate from the generic `params` list to avoid costly recomputes.
timestampParams: ({ createdAtDate, createdAtDateTS, timeagoFormattedDate, absoluteFormattedDate }) => ({
createdAtDate,
createdAtDateTS,
timeagoFormattedDate,
absoluteFormattedDate
}),
// This params list deliberately does *not* include `spoilersShown` or `replyShown`, because these
// change frequently and would therefore cause costly recomputes if included here.
// The main goal here is to avoid typing by passing as many params as possible to child components.
params: ({ notification, notificationId, status, statusId, timelineType,
account, accountId, uuid, isStatusInNotification, isStatusInOwnThread,
originalAccount, originalAccountId, visibility,
originalAccount, originalAccountId, spoilerShown, visibility, replyShown,
replyVisibility, spoilerText, originalStatus, originalStatusId, inReplyToId,
enableShortcuts, shortcutScope, originalStatusEmojis }) => ({
createdAtDate, timeagoFormattedDate, enableShortcuts, absoluteFormattedDate, shortcutScope }) => ({
notification,
notificationId,
status,
@ -337,15 +314,19 @@
isStatusInOwnThread,
originalAccount,
originalAccountId,
spoilerShown,
visibility,
replyShown,
replyVisibility,
spoilerText,
originalStatus,
originalStatusId,
inReplyToId,
createdAtDate,
timeagoFormattedDate,
enableShortcuts,
shortcutScope,
originalStatusEmojis
absoluteFormattedDate,
shortcutScope
})
},
events: {

View File

@ -1,6 +1,6 @@
<a ref:cardlink href={url} class="status-card" target="_blank" rel="noopener noreferrer">
<strong class="card-title">
{unescapedTitle}
{title}
</strong>
{#if description}
<div class="card-content">
@ -10,7 +10,7 @@
</div>
{/if}
<span class="card-description">
{unescapedDescription}
{description}
</span>
</div>
{/if}
@ -87,7 +87,6 @@
<script>
import LazyImage from '../LazyImage.html'
import Shortcut from '../shortcut/Shortcut.html'
import { unescape } from '../../_thirdparty/unescape/unescape'
export default {
components: {
@ -97,11 +96,9 @@
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: {

View File

@ -76,9 +76,8 @@
)
},
content: ({ originalStatus }) => (originalStatus.content || ''),
massagedContent: ({ content, originalStatusEmojis, $autoplayGifs }) => (
massageUserText(content, originalStatusEmojis, $autoplayGifs)
)
emojis: ({ originalStatus }) => originalStatus.emojis,
massagedContent: ({ content, emojis, $autoplayGifs }) => massageUserText(content, emojis, $autoplayGifs)
},
methods: {
hydrateContent () {

View File

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

View File

@ -1,5 +1,5 @@
<div class="status-header {isStatusInNotification ? 'status-in-notification' : ''} {notificationType === 'follow' ? 'header-is-follow' : ''}">
<div class="status-header-avatar {timelineType === 'pinned' || notificationType === 'poll' ? 'hidden' : ''}">
<div class="status-header {isStatusInNotification ? 'status-in-notification' : ''} {notification && notification.type === 'follow' ? 'header-is-follow' : ''}">
<div class="status-header-avatar {timelineType === 'pinned' ? 'hidden' : ''}">
<Avatar {account} size="extra-small"/>
</div>
<SvgIcon className="status-header-svg" href={icon} />
@ -9,7 +9,7 @@
<span class="status-header-author">
Pinned toot
</span>
{:elseif notificationType !== 'poll'}
{:else}
<a id={elementId}
href="/accounts/{accountId}"
rel="prefetch"
@ -20,7 +20,17 @@
</a>
{/if}
<span class="status-header-action">{actionText}</span>
<span class="status-header-action">
{#if notification && notification.type === 'reblog'}
boosted your status
{:elseif notification && notification.type === 'favourite'}
favorited your status
{:elseif notification && notification.type === 'follow'}
followed you
{:elseif status && status.reblog}
boosted
{/if}
</span>
</div>
</div>
<style>
@ -95,7 +105,6 @@
import Avatar from '../Avatar.html'
import AccountDisplayName from '../profile/AccountDisplayName.html'
import SvgIcon from '../SvgIcon.html'
import { store } from '../../_store/store'
export default {
components: {
@ -103,40 +112,17 @@
AccountDisplayName,
SvgIcon
},
store: () => store,
computed: {
elementId: ({ uuid }) => `status-header-${uuid}`,
notificationType: ({ notification }) => notification && notification.type,
icon: ({ notificationType, status, timelineType }) => {
icon: ({ notification, status, timelineType }) => {
if (timelineType === 'pinned') {
return '#fa-thumb-tack'
} else if ((notificationType === 'reblog') || (status && status.reblog)) {
} else if ((notification && notification.type === 'reblog') || (status && status.reblog)) {
return '#fa-retweet'
} else if (notificationType === 'follow') {
} else if (notification && notification.type === 'follow') {
return '#fa-user-plus'
} else if (notificationType === 'poll') {
return '#fa-bar-chart'
}
return '#fa-star'
},
actionText: ({ notificationType, status, $currentVerifyCredentials }) => {
if (notificationType === 'reblog') {
return 'boosted your status'
} else if (notificationType === 'favourite') {
return 'favorited your status'
} else if (notificationType === 'follow') {
return 'followed you'
} else if (notificationType === 'poll') {
if ($currentVerifyCredentials && status && $currentVerifyCredentials.id === status.account.id) {
return 'A poll you created has ended'
} else {
return 'A poll you voted on has ended'
}
} else if (status && status.reblog) {
return 'boosted'
} else {
return ''
}
}
}
}

View File

@ -1,327 +0,0 @@
<div class={computedClass} aria-busy={loading} >
{#if voted || expired }
<ul aria-label="Poll results">
{#each options as option}
<li class="option">
<div class="option-text">
<strong>{option.share}%</strong> {option.title}
</div>
<svg aria-hidden="true">
<line x1="0" y1="0" x2="{option.share}%" y2="0" />
</svg>
</li>
{/each}
</ul>
{:else}
<form class="poll-form" aria-label="Vote on poll" on:submit="onSubmit(event)" ref:form>
<ul aria-label="Poll choices">
{#each options as option, i}
<li class="poll-form-option">
<input type="{multiple ? 'checkbox' : 'radio'}"
id="poll-choice-{uuid}-{i}"
name="poll-choice-{uuid}"
value="{i}"
on:change="onChange()"
>
<label for="poll-choice-{uuid}-{i}">
{option.title}
</label>
</li>
{/each}
</ul>
<button disabled={formDisabled} type="submit">Vote</button>
</form>
{/if}
<div class="poll-details">
<div class="poll-stat">
<SvgIcon className="poll-icon" href="#fa-bar-chart" />
<span class="poll-stat-text">{votesCount} {votesCount === 1 ? 'vote' : 'votes'}</span>
</div>
<div class="poll-stat">
<SvgIcon className="poll-icon" href="#fa-clock" />
<span class="poll-stat-text poll-stat-expiry">
<span class="{useNarrowSize ? 'sr-only' : ''}">{expiryText}</span>
<time datetime={expiresAt} title={expiresAtAbsoluteFormatted}>
{expiresAtTimeagoFormatted}
</time>
</span>
</div>
<button class="poll-stat {expired ? 'poll-expired' : ''}" id={refreshElementId}>
<SvgIcon className="poll-icon" href="#fa-refresh" />
<span class="poll-stat-text">
Refresh
</span>
</button>
</div>
</div>
<style>
.poll {
grid-area: poll;
margin: 10px 10px 10px 5px;
padding: 20px;
border: 1px solid var(--main-border);
border-radius: 2px;
transition: opacity 0.2s linear;
}
.poll.status-in-own-thread {
padding: 20px;
}
.poll.poll-loading {
opacity: 0.5;
pointer-events: none;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
li {
margin: 0;
padding: 0;
}
.option {
margin: 0 0 10px 0;
padding: 0;
display: flex;
flex-direction: column;
stroke: var(--svg-fill);
stroke-width: 10px;
}
.option-text {
word-wrap: break-word;
white-space: pre-wrap;
font-size: 1.1em;
}
svg {
height: 10px;
width: 100%;
margin-top: 5px;
}
.status-in-notification .option-text {
color: var(--very-deemphasized-text-color);
}
.status-in-notification svg {
opacity: 0.5;
}
.status-in-own-thread .option-text {
font-size: 1.2em;
}
.poll-details {
display: grid;
grid-template-columns: max-content minmax(0, max-content) max-content;
grid-gap: 20px;
align-items: center;
justify-content: left;
margin-top: 10px;
}
button.poll-stat {
/* reset button styles */
background: none;
box-shadow: none;
border: none;
border-spacing: 0;
margin: 0;
padding: 0;
font-size: inherit;
font-weight: normal;
text-align: left;
text-decoration: none;
text-indent: 0;
}
button.poll-stat:hover {
text-decoration: underline;
}
.poll-stat {
display: flex;
flex-direction: row;
align-items: center;
color: var(--deemphasized-text-color);
}
.poll-stat.poll-expired {
display: none;
}
.poll-stat-text {
margin-left: 5px;
}
.poll-stat-expiry {
word-wrap: break-word;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
:global(.poll-icon) {
fill: var(--deemphasized-text-color);
width: 18px;
height: 18px;
min-width: 18px;
}
.poll-form-option {
padding-bottom: 10px;
}
.poll-form button {
}
.poll-form label {
text-overflow: ellipsis;
overflow: hidden;
word-break: break-word;
white-space: pre-wrap;
padding-left: 5px;
}
@media (max-width: 479px) {
.poll {
padding: 10px 5px;
}
.poll.status-in-own-thread {
padding: 10px;
}
.poll-details {
grid-gap: 5px;
justify-content: space-between;
}
}
</style>
<script>
import SvgIcon from '../SvgIcon.html'
import { store } from '../../_store/store'
import { formatTimeagoFutureDate, formatTimeagoDate } from '../../_intl/formatTimeagoDate'
import { absoluteDateFormatter } from '../../_utils/formatters'
import { registerClickDelegate } from '../../_utils/delegate'
import { classname } from '../../_utils/classname'
import { getPoll, voteOnPoll } from '../../_actions/polls'
const REFRESH_MIN_DELAY = 1000
async function doAsyncActionWithDelay (func) {
let start = Date.now()
let res = await func()
let timeElapsed = Date.now() - start
if (timeElapsed < REFRESH_MIN_DELAY) {
// If less than five seconds, then continue to show the loading animation
// so it's clear that something happened.
await new Promise(resolve => setTimeout(resolve, REFRESH_MIN_DELAY - timeElapsed))
}
return res
}
function getChoices (form, options) {
let res = []
for (let i = 0; i < options.length; i++) {
if (form.elements[i].checked) {
res.push(i)
}
}
return res
}
export default {
oncreate () {
this.onRefreshClick = this.onRefreshClick.bind(this)
let { refreshElementId } = this.get()
registerClickDelegate(this, refreshElementId, this.onRefreshClick)
},
data: () => ({
loading: false,
choices: []
}),
store: () => store,
computed: {
pollId: ({ originalStatus }) => originalStatus.poll.id,
poll: ({ originalStatus, $polls, pollId }) => $polls[pollId] || originalStatus.poll,
options: ({ poll }) => poll.options.map(({ title, votes_count: votesCount }) => ({
title,
share: poll.votes_count ? Math.round(votesCount / poll.votes_count * 100) : 0
})),
votesCount: ({ poll }) => poll.votes_count,
voted: ({ poll }) => poll.voted,
multiple: ({ poll }) => poll.multiple,
expired: ({ poll }) => poll.expired,
expiresAt: ({ poll }) => poll.expires_at,
expiresAtTS: ({ expiresAt }) => new Date(expiresAt).getTime(),
expiresAtTimeagoFormatted: ({ expiresAtTS, expired, $now }) => (
expired ? formatTimeagoDate(expiresAtTS, $now) : formatTimeagoFutureDate(expiresAtTS, $now)
),
expiresAtAbsoluteFormatted: ({ expiresAtTS }) => absoluteDateFormatter.format(expiresAtTS),
expiryText: ({ expired }) => expired ? 'Ended' : 'Ends',
refreshElementId: ({ uuid }) => `poll-refresh-${uuid}`,
useNarrowSize: ({ $isMobileSize, expired }) => $isMobileSize && !expired,
formDisabled: ({ choices }) => !choices.length,
computedClass: ({ isStatusInNotification, isStatusInOwnThread, loading }) => (
classname(
'poll',
isStatusInNotification && 'status-in-notification',
isStatusInOwnThread && 'status-in-own-thread',
loading && 'poll-loading'
)
)
},
methods: {
async onRefreshClick (e) {
e.preventDefault()
e.stopPropagation()
let { pollId } = this.get()
this.set({ loading: true })
try {
let poll = await doAsyncActionWithDelay(() => getPoll(pollId))
if (poll) {
let { polls } = this.store.get()
polls[pollId] = poll
this.store.set({ polls })
}
} finally {
this.set({ loading: false })
}
},
async onSubmit (e) {
e.preventDefault()
e.stopPropagation()
let { pollId, options, formDisabled } = this.get()
if (formDisabled) {
return
}
let choices = getChoices(this.refs.form, options)
this.set({ loading: true })
try {
let poll = await doAsyncActionWithDelay(() => voteOnPoll(pollId, choices))
if (poll) {
let { polls } = this.store.get()
polls[pollId] = poll
this.store.set({ polls })
}
} finally {
this.set({ loading: false })
}
},
onChange () {
let { options } = this.get()
let choices = getChoices(this.refs.form, options)
this.set({ choices: choices })
}
},
components: {
SvgIcon
}
}
</script>

View File

@ -64,9 +64,10 @@
Shortcut
},
computed: {
massagedSpoilerText: ({ spoilerText, originalStatusEmojis, $autoplayGifs }) => {
emojis: ({ originalStatus }) => originalStatus.emojis,
massagedSpoilerText: ({ spoilerText, emojis, $autoplayGifs }) => {
spoilerText = escapeHtml(spoilerText)
return emojifyText(spoilerText, originalStatusEmojis, $autoplayGifs)
return emojifyText(spoilerText, emojis, $autoplayGifs)
},
elementId: ({ uuid }) => `spoiler-${uuid}`
},

View File

@ -1,100 +1,32 @@
<div class="loading-footer {shown ? '' : 'hidden'}">
<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>
<LoadingSpinner size={48} />
<span class="loading-footer-info">
Loading more...
</span>
</div>
<style>
.loading-footer {
padding: 20px 0;
padding: 20px 0 10px;
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, $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)
}
}
shown: ({ $timelineInitialized, $runningUpdate }) => ($timelineInitialized && $runningUpdate)
},
components: {
LoadingSpinner
}
}
</script>
</script>

View File

@ -45,7 +45,7 @@
} from '../../_utils/asyncModules'
import { timelines } from '../../_static/timelines'
import {
fetchMoreItemsAtBottomOfTimeline,
fetchTimelineItemsOnScrollToBottom,
setupTimeline,
showMoreItemsForTimeline,
showMoreItemsForThread,
@ -127,11 +127,13 @@
timelineValue !== $firstTimelineItemId &&
timelineValue
),
itemIds: ({ $filteredTimelineItemSummaries }) => (
$filteredTimelineItemSummaries && $filteredTimelineItemSummaries.map(_ => _.id)
itemIds: ({ $timelineItemSummaries }) => (
// TODO: filter
$timelineItemSummaries && $timelineItemSummaries.map(_ => _.id)
),
itemIdsToAdd: ({ $filteredTimelineItemSummariesToAdd }) => (
$filteredTimelineItemSummariesToAdd && $filteredTimelineItemSummariesToAdd.map(_ => _.id)
itemIdsToAdd: ({ $timelineItemSummariesToAdd }) => (
// TODO: filter
$timelineItemSummariesToAdd && $timelineItemSummariesToAdd.map(_ => _.id)
),
headerProps: ({ itemIdsToAdd }) => {
return {
@ -165,16 +167,18 @@
},
onScrollToBottom () {
let { timelineType } = this.get()
let { timelineInitialized, runningUpdate, disableInfiniteScroll } = this.store.get()
let { timelineInitialized, runningUpdate } = 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()
/* no await */ fetchMoreItemsAtBottomOfTimeline(currentInstance, timeline)
fetchTimelineItemsOnScrollToBottom(
currentInstance,
timeline
)
},
onScrollToTop () {
let { shouldShowHeader } = this.store.get()
@ -186,7 +190,7 @@
}
},
setupStreaming () {
let { currentInstance, disableInfiniteScroll } = this.store.get()
let { currentInstance } = this.store.get()
let { timeline, timelineType } = this.get()
let handleItemIdsToAdd = () => {
let { itemIdsToAdd } = this.get()
@ -202,17 +206,13 @@
if (timelineType === 'status') {
// this is a thread, just insert the statuses already
showMoreItemsForThread(currentInstance, timeline)
} else if (!disableInfiniteScroll && scrollTop === 0 && !shouldShowHeader && !showHeader) {
} else if (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')
}

View File

@ -1,20 +1,9 @@
import { format } from '../_thirdparty/timeago/timeago'
import { mark, stop } from '../_utils/marks'
// Format a date in the past
export function formatTimeagoDate (date, now) {
export function formatTimeagoDate (date) {
mark('formatTimeagoDate')
// use Math.max() to avoid things like "in 10 seconds" when the timestamps are slightly off
let res = format(date, Math.max(now, date))
let res = format(date)
stop('formatTimeagoDate')
return res
}
// Format a date in the future
export function formatTimeagoFutureDate (date, now) {
mark('formatTimeagoFutureDate')
// use Math.min() for same reason as above
let res = format(date, Math.min(now, date))
stop('formatTimeagoFutureDate')
return res
}

View File

@ -3,15 +3,15 @@
<h2>Media</h2>
<form class="ui-settings">
<div class="setting-group">
<input type="checkbox" id="choice-never-mark-media-sensitive"
bind:checked="$neverMarkMediaAsSensitive" on:change="onChange(event)">
<label for="choice-never-mark-media-sensitive">Show sensitive media by default</label>
</div>
<div class="setting-group">
<input type="checkbox" id="choice-mark-media-sensitive"
bind:checked="$markMediaAsSensitive" on:change="onChange(event)">
<label for="choice-mark-media-sensitive">Hide all media by default</label>
<label for="choice-mark-media-sensitive">Always mark media as sensitive</label>
</div>
<div class="setting-group">
<input type="checkbox" id="choice-never-mark-media-sensitive"
bind:checked="$neverMarkMediaAsSensitive" on:change="onChange(event)">
<label for="choice-never-mark-media-sensitive">Never mark media as sensitive</label>
</div>
<div class="setting-group">
<input type="checkbox" id="choice-large-inline-media"
@ -32,19 +32,6 @@
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)">
@ -102,13 +89,11 @@
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,
Tooltip
ThemeSettings
},
methods: {
onChange (event) {

View File

@ -8,9 +8,6 @@
<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

@ -4,13 +4,9 @@
{#if verifyCredentials}
<h2>Logged in as:</h2>
<InstanceUserProfile {verifyCredentials} />
<h2>Home timeline filters</h2>
<HomeTimelineFilterSettings {instanceName} />
<h2>Notification filters</h2>
<NotificationFilterSettings {instanceName} />
<h2>Push notifications</h2>
<h2>Push notifications:</h2>
<PushNotificationSettings {instanceName} />
<h2>Theme</h2>
<h2>Theme:</h2>
<ThemeSettings {instanceName} />
<InstanceActions {instanceName} />
@ -27,8 +23,6 @@
import { store } from '../../../_store/store'
import SettingsLayout from '../../../_components/settings/SettingsLayout.html'
import InstanceUserProfile from '../../../_components/settings/instance/InstanceUserProfile.html'
import HomeTimelineFilterSettings from '../../../_components/settings/instance/HomeTimelineFilterSettings.html'
import NotificationFilterSettings from '../../../_components/settings/instance/NotificationFilterSettings.html'
import PushNotificationSettings from '../../../_components/settings/instance/PushNotificationSettings.html'
import ThemeSettings from '../../../_components/settings/instance/ThemeSettings.html'
import InstanceActions from '../../../_components/settings/instance/InstanceActions.html'
@ -49,9 +43,7 @@
InstanceUserProfile,
PushNotificationSettings,
ThemeSettings,
InstanceActions,
HomeTimelineFilterSettings,
NotificationFilterSettings
InstanceActions
}
}
</script>
</script>

View File

@ -1,154 +0,0 @@
<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

@ -1,8 +0,0 @@
export const HOME_REBLOGS = 'homeReblogs'
export const HOME_REPLIES = 'homeReplies'
export const NOTIFICATION_REBLOGS = 'notificationReblogs'
export const NOTIFICATION_FAVORITES = 'notificationFavs'
export const NOTIFICATION_FOLLOWS = 'notificationFollows'
export const NOTIFICATION_MENTIONS = 'notificationMentions'
export const NOTIFICATION_POLLS = 'notificationPolls'

View File

@ -1,32 +0,0 @@
export const POLL_EXPIRY_OPTIONS = [
{
'value': 300,
'label': '5 minutes'
},
{
'value': 1800,
'label': '30 minutes'
},
{
'value': 3600,
'label': '1 hour'
},
{
'value': 21600,
'label': '6 hours'
},
{
'value': 86400,
'label': '1 day'
},
{
'value': 259200,
'label': '3 days'
},
{
'value': 604800,
'label': '7 days'
}
]
export const POLL_EXPIRY_DEFAULT = 86400

View File

@ -42,10 +42,16 @@ const themes = [
color: '#4ab92f'
},
{
name: 'grayscale',
label: 'Grayscale',
name: 'sam',
label: 'Sam',
dark: false,
color: '#999999'
color: '#ffffea'
},
{
name: 'sam-sepia',
label: 'Sam (sepia)',
dark: false,
color: '#ffffea',
},
{
name: 'ozark',
@ -94,12 +100,6 @@ const themes = [
label: 'Pitch Black',
dark: true,
color: '#000'
},
{
name: 'dark-grayscale',
label: 'Dark Grayscale',
dark: true,
color: '#666'
}
]

View File

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

View File

@ -1,14 +1,5 @@
import { get } from '../../_utils/lodash-lite'
import { getFirstIdFromItemSummaries, getLastIdFromItemSummaries } from '../../_utils/getIdFromItemSummaries'
import {
HOME_REBLOGS,
HOME_REPLIES,
NOTIFICATION_REBLOGS,
NOTIFICATION_FOLLOWS,
NOTIFICATION_FAVORITES,
NOTIFICATION_POLLS,
NOTIFICATION_MENTIONS
} from '../../_static/instanceSettings'
function computeForTimeline (store, key, defaultValue) {
store.compute(key,
@ -19,31 +10,6 @@ function computeForTimeline (store, key, defaultValue) {
)
}
// Compute just the boolean, e.g. 'showPolls', so that we can use that boolean as
// the input to the timelineFilterFunction computations. This should reduce the need to
// re-compute the timelineFilterFunction over and over.
function computeTimelineFilter (store, computationName, timelinesToSettingsKeys) {
store.compute(
computationName,
['currentInstance', 'instanceSettings', 'currentTimeline'],
(currentInstance, instanceSettings, currentTimeline) => {
let settingsKey = timelinesToSettingsKeys[currentTimeline]
return settingsKey ? get(instanceSettings, [currentInstance, settingsKey], true) : true
}
)
}
// Ditto for notifications, which we always have to keep track of due to the notification count.
function computeNotificationFilter (store, computationName, key) {
store.compute(
computationName,
['currentInstance', 'instanceSettings'],
(currentInstance, instanceSettings) => {
return get(instanceSettings, [currentInstance, key], true)
}
)
}
export function timelineComputations (store) {
computeForTimeline(store, 'timelineItemSummaries', null)
computeForTimeline(store, 'timelineItemSummariesToAdd', null)
@ -75,110 +41,16 @@ export function timelineComputations (store) {
getLastIdFromItemSummaries(timelineItemSummaries)
))
computeTimelineFilter(store, 'timelineShowReblogs', { home: HOME_REBLOGS, notifications: NOTIFICATION_REBLOGS })
computeTimelineFilter(store, 'timelineShowReplies', { home: HOME_REPLIES })
computeTimelineFilter(store, 'timelineShowFollows', { notifications: NOTIFICATION_FOLLOWS })
computeTimelineFilter(store, 'timelineShowFavs', { notifications: NOTIFICATION_FAVORITES })
computeTimelineFilter(store, 'timelineShowMentions', { notifications: NOTIFICATION_MENTIONS })
computeTimelineFilter(store, 'timelineShowPolls', { notifications: NOTIFICATION_POLLS })
computeNotificationFilter(store, 'timelineNotificationShowReblogs', NOTIFICATION_REBLOGS)
computeNotificationFilter(store, 'timelineNotificationShowFollows', NOTIFICATION_FOLLOWS)
computeNotificationFilter(store, 'timelineNotificationShowFavs', NOTIFICATION_FAVORITES)
computeNotificationFilter(store, 'timelineNotificationShowMentions', NOTIFICATION_MENTIONS)
computeNotificationFilter(store, 'timelineNotificationShowPolls', NOTIFICATION_POLLS)
function createFilterFunction (showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls) {
return item => {
switch (item.type) {
case 'poll':
return showPolls
case 'favourite':
return showFavs
case 'reblog':
return showReblogs
case 'mention':
return showMentions
case 'follow':
return showFollows
}
if (item.reblogId) {
return showReblogs
} else if (item.replyId) {
return showReplies
} else {
return true
}
}
}
store.compute(
'timelineFilterFunction',
[
'timelineShowReblogs', 'timelineShowReplies', 'timelineShowFollows',
'timelineShowFavs', 'timelineShowMentions', 'timelineShowPolls'
],
(showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls) => (
createFilterFunction(showReblogs, showReplies, showFollows, showFavs, showMentions, showPolls)
)
)
store.compute(
'timelineNotificationFilterFunction',
[
'timelineNotificationShowReblogs', 'timelineNotificationShowFollows',
'timelineNotificationShowFavs', 'timelineNotificationShowMentions',
'timelineNotificationShowPolls'
],
(showReblogs, showFollows, showFavs, showMentions, showPolls) => (
createFilterFunction(showReblogs, true, showFollows, showFavs, showMentions, showPolls)
)
)
store.compute(
'filteredTimelineItemSummaries',
['timelineItemSummaries', 'timelineFilterFunction'],
(timelineItemSummaries, timelineFilterFunction) => {
return timelineItemSummaries && timelineItemSummaries.filter(timelineFilterFunction)
}
)
store.compute(
'filteredTimelineItemSummariesToAdd',
['timelineItemSummariesToAdd', 'timelineFilterFunction'],
(timelineItemSummariesToAdd, timelineFilterFunction) => {
return timelineItemSummariesToAdd && timelineItemSummariesToAdd.filter(timelineFilterFunction)
}
)
store.compute('timelineNotificationItemSummaries',
[`timelineData_timelineItemSummariesToAdd`, 'timelineFilterFunction', 'currentInstance'],
(root, timelineFilterFunction, currentInstance) => (
get(root, [currentInstance, 'notifications'])
)
)
store.compute(
'filteredTimelineNotificationItemSummaries',
['timelineNotificationItemSummaries', 'timelineNotificationFilterFunction'],
(timelineNotificationItemSummaries, timelineNotificationFilterFunction) => (
timelineNotificationItemSummaries && timelineNotificationItemSummaries.filter(timelineNotificationFilterFunction)
)
)
store.compute('numberOfNotifications',
['filteredTimelineNotificationItemSummaries', 'disableNotificationBadge'],
(filteredTimelineNotificationItemSummaries, disableNotificationBadge) => (
(!disableNotificationBadge && filteredTimelineNotificationItemSummaries)
? filteredTimelineNotificationItemSummaries.length
: 0
[`timelineData_timelineItemSummariesToAdd`, 'currentInstance'],
(root, currentInstance) => (
(root && root[currentInstance] && root[currentInstance].notifications &&
root[currentInstance].notifications.length) || 0
)
)
store.compute('hasNotifications',
['numberOfNotifications', 'currentPage'],
(numberOfNotifications, currentPage) => (
currentPage !== 'notifications' && !!numberOfNotifications
)
(numberOfNotifications, currentPage) => currentPage !== 'notifications' && !!numberOfNotifications
)
}

View File

@ -1,5 +1,3 @@
import { get } from '../../_utils/lodash-lite'
export function instanceMixins (Store) {
Store.prototype.setComposeData = function (realm, obj) {
let { composeData, currentInstance } = this.get()
@ -22,18 +20,4 @@ export function instanceMixins (Store) {
}
this.set({ composeData })
}
Store.prototype.getInstanceSetting = function (instanceName, settingName, defaultValue) {
let { instanceSettings } = this.get()
return get(instanceSettings, [instanceName, settingName], defaultValue)
}
Store.prototype.setInstanceSetting = function (instanceName, settingName, value) {
let { instanceSettings } = this.get()
if (!instanceSettings[instanceName]) {
instanceSettings[instanceName] = {}
}
instanceSettings[instanceName][settingName] = value
this.set({ instanceSettings })
}
}

View File

@ -1,16 +0,0 @@
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)
})
}

View File

@ -1,47 +0,0 @@
// For convenience, periodically re-compute the current time. This ensures freshness of
// displays like "x minutes ago" without having to jump through a lot of hoops.
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
import lifecycle from 'page-lifecycle/dist/lifecycle.mjs'
const POLL_INTERVAL = 10000
export function nowObservers (store) {
let interval
function updateNow () {
store.set({ now: Date.now() })
}
function startPolling () {
interval = setInterval(() => scheduleIdleTask(updateNow), POLL_INTERVAL)
}
function stopPolling () {
if (interval) {
clearInterval(interval)
interval = null
}
}
function restartPolling () {
stopPolling()
scheduleIdleTask(updateNow)
startPolling()
}
updateNow()
if (process.browser) {
startPolling()
lifecycle.addEventListener('statechange', e => {
if (e.newState === 'passive') {
console.log('stopping Date.now() observer...')
stopPolling()
} else if (e.newState === 'active') {
console.log('restarting Date.now() observer...')
restartPolling()
}
})
}
}

View File

@ -1,21 +1,17 @@
import { onlineObservers } from './onlineObservers'
import { nowObservers } from './nowObservers'
import { navObservers } from './navObservers'
import { pageVisibilityObservers } from './pageVisibilityObservers'
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)
nowObservers(store)
navObservers(store)
pageVisibilityObservers(store)
resizeObservers(store)
touchObservers(store)
logOutObservers(store)
grayscaleObservers(store)
setupLoggedInObservers(store)
}

View File

@ -6,6 +6,8 @@ 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) {
@ -17,9 +19,20 @@ export function onlineObservers (store) {
if (!process.browser) {
return
}
let meta = document.getElementById('theThemeColor')
let oldTheme = meta.content
store.observe('online', online => {
if (!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
notifyOffline()
}
})

View File

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

View File

@ -12,20 +12,13 @@ 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: '',
instanceThemes: {},
instanceSettings: {},
loggedInInstances: {},
loggedInInstancesInOrder: [],
markMediaAsSensitive: false,
@ -45,7 +38,6 @@ const nonPersistedState = {
instanceLists: {},
online: !process.browser || navigator.onLine,
pinnedStatuses: {},
polls: {},
pushNotificationsSupport:
process.browser &&
('serviceWorker' in navigator &&

View File

@ -1,20 +0,0 @@
// "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]
}
})
}
}

View File

@ -4,7 +4,7 @@
* Contract: i@hust.cc
*/
var IndexMapEn = ['second', 'minute', 'hour', 'day', 'week', 'month', 'year']
var IndexMapEn = 'second_minute_hour_day_week_month_year'.split('_')
var SEC_ARRAY = [60, 60, 24, 7, 365 / 7 / 12, 12]
/**
@ -63,14 +63,16 @@ function formatDiff (diff) {
* @param nowDate
* @returns {number}
*/
function diffSec (date, now) {
return (now - date) / 1000
function diffSec (date) {
var nowDate = new Date()
var otherDate = new Date(date)
return (nowDate - otherDate) / 1000
}
/**
* Created by hustcc on 18/5/20.
* Contract: i@hust.cc
*/
export function format (date, now) {
return formatDiff(diffSec(date, now))
export function format (date) {
return formatDiff(diffSec(date))
}

View File

@ -1,21 +0,0 @@
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.

View File

@ -1,63 +0,0 @@
// via https://github.com/jonschlinkert/unescape/blob/98d1e52/index.js
const chars = {
'&quot;': '"',
'&#34;': '"',
'&apos;': '\'',
'&#39;': '\'',
'&amp;': '&',
'&#38;': '&',
'&gt;': '>',
'&#62;': '>',
'&lt;': '<',
'&#60;': '<',
'&cent;': '¢',
'&#162;': '¢',
'&copy;': '©',
'&#169;': '©',
'&euro;': '€',
'&#8364;': '€',
'&pound;': '£',
'&#163;': '£',
'&reg;': '®',
'&#174;': '®',
'&yen;': '¥',
'&#165;': '¥',
'&nbsp;': ' '
}
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 }

View File

@ -1,17 +0,0 @@
import { slide as svelteSlide } from 'svelte-transitions'
import { store } from '../_store/store'
import noop from 'lodash-es/noop'
// same as svelte-transitions, but respecting reduceMotion
export function slide (node, ref) {
let { reduceMotion } = store.get()
if (reduceMotion) {
return {
delay: 0,
duration: 1, // setting to 0 causes some kind of built-in duration
easing: _ => _,
css: noop
}
}
return svelteSlide(node, ref)
}

View File

@ -43,7 +43,3 @@ 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)

View File

@ -1,22 +1,11 @@
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', async () => {
await skipWaiting()
document.location.reload(true)
})
snackbar.announce('App update available.', 'Reload', () => document.location.reload(true))
}
})
}

View File

@ -1,9 +1,9 @@
const prefersDarkTheme = process.browser && window.matchMedia('(prefers-color-scheme: dark)').matches
const meta = process.browser && document.getElementById('theThemeColor')
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
export const INLINE_THEME = 'default' // theme that does not require external CSS
export const DEFAULT_LIGHT_THEME = 'default' // theme that is shown by default
export const DEFAULT_DARK_THEME = 'ozark' // theme that is shown for prefers-color-scheme:dark
export const DEFAULT_LIGHT_THEME = 'default'
export const DEFAULT_DARK_THEME = 'ozark'
export const DEFAULT_THEME = prefersDarkTheme ? DEFAULT_DARK_THEME : DEFAULT_LIGHT_THEME
function getExistingThemeLink () {
@ -31,16 +31,14 @@ function loadCSS (href) {
}
})
document.head.appendChild(link)
// inserting before the offline <style> ensures that the offline style wins when offline
document.head.insertBefore(link, offlineStyle)
}
export function switchToTheme (themeName = DEFAULT_THEME, enableGrayscale) {
if (enableGrayscale) {
themeName = prefersDarkTheme ? 'grayscale-dark' : 'grayscale'
}
export function switchToTheme (themeName = DEFAULT_THEME) {
let themeColor = window.__themeColors[themeName]
meta.content = themeColor || window.__themeColors[DEFAULT_THEME]
if (themeName !== INLINE_THEME) {
if (themeName !== DEFAULT_LIGHT_THEME) {
loadCSS(`/theme-${themeName}.css`)
} else {
resetExistingTheme()

View File

@ -1,20 +0,0 @@
<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

@ -87,7 +87,7 @@
--status-direct-background: #{darken($body-bg-color, 5%)};
--main-theme-color: #{$main-theme-color};
--warning-color: #{#e01f19};
--alt-input-bg: #{rgba($main-bg-color, 0.9)};
--alt-input-bg: #{rgba($main-bg-color, 0.7)};
--muted-modal-text: #{$secondary-text-color};
--muted-modal-bg: #{transparent};
@ -112,10 +112,4 @@
--tooltip-bg: rgba(30, 30, 30, 0.9);
--tooltip-color: white;
--floating-button-bg: #{rgba($main-bg-color, 0.8)};
--floating-button-bg-hover: #{darken(rgba($main-bg-color, 0.9), 5%)};
--floating-button-bg-active: #{darken(rgba($main-bg-color, 0.9), 10%)};
--length-indicator-color: #{$main-theme-color};
}

View File

@ -18,7 +18,7 @@
--status-direct-background: #{darken($body-bg-color, 5%)};
--main-theme-color: #{$main-theme-color};
--warning-color: #{#c7423d};
--alt-input-bg: #{rgba($main-bg-color, 0.9)};
--alt-input-bg: #{rgba($main-bg-color, 0.7)};
--muted-modal-bg: #{transparent};
--muted-modal-focus: #{#999};
@ -46,6 +46,4 @@
--tab-bg-hover-non-selected: #{darken($main-bg-color, 1%)};
--toast-anchor-color: #{$anchor-color};
--length-indicator-color: var(--action-button-fill-color);
}

View File

@ -1,6 +1,6 @@
$main-theme-color: #666;
$main-theme-color: #999999;
$body-bg-color: lighten($main-theme-color, 38%);
$anchor-color: lighten($main-theme-color, 5%);
$anchor-color: $main-theme-color;
$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";

View File

@ -17,11 +17,7 @@ $compose-background: lighten($main-theme-color, 32%);
:root {
--settings-list-item-text: #{$main-text-color};
--settings-list-item-text-hover: #{$main-text-color};
--action-button-fill-color: #{lighten($main-theme-color, 30%)};
--action-button-fill-color-hover: #{lighten($main-theme-color, 35%)};
--action-button-fill-color-active: #{lighten($main-theme-color, 40%)};
--action-button-fill-color-pressed: #{$anchor-color};
--action-button-fill-color-pressed-hover: #{darken($anchor-color, 2%)};
--action-button-fill-color-pressed-active: #{darken($anchor-color, 15%)};
@ -34,4 +30,4 @@ $compose-background: lighten($main-theme-color, 32%);
--nav-text-color-hover: #{$main-text-color};
--nav-a-selected-border: #{$anchor-color};
--nav-a-selected-border-hover: #{$anchor-color};
}
}

View File

@ -1,16 +0,0 @@
$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

@ -29,9 +29,9 @@ $compose-background: darken($main-theme-color, 12%);
--form-border: #{darken($border-color, 10%)};
--action-button-fill-color: #{lighten($main-theme-color, 5%)};
--action-button-fill-color-hover: #{lighten($main-theme-color, 12%)};
--action-button-fill-color-active: #{darken($main-theme-color, 15%)};
--action-button-fill-color: #{$main-theme-color};
--action-button-fill-color-hover: #{lighten($main-theme-color, 4%)};
--action-button-fill-color-active: #{darken($main-theme-color, 13%)};
--action-button-fill-color-pressed: #{lighten($main-theme-color, 20%)};
--action-button-fill-color-pressed-hover: #{lighten($main-theme-color, 24%)};
--action-button-fill-color-pressed-active: #{lighten($main-theme-color, 7%)};

View File

@ -0,0 +1,5 @@
@import "sam.scss";
img {
filter: sepia(100%);
}

58
src/scss/themes/sam.scss Normal file
View File

@ -0,0 +1,58 @@
$main-theme-color: #99994C;
$body-bg-color: #FFFFFF;
$main-bg-color: #FFFFEA;
$main-text-color: #000000;
$border-color: #99994C;
$secondary-text-color: #444444;
$anchor-color: #444400;
$toast-border: #99994C;
$toast-bg: #444400;
$focus-outline: #99994C;
$compose-background: lighten($main-theme-color, 32%);
@import "_base.scss";
$scrollbar-face: #FFFFEA;
$scrollbar-face-hover: #FFFFEA;
$scrollbar-face-active: #FFFFEA;
$scrollbar-track: #99994C;
@import "_scrollbars.scss";
:root {
--deemphasized-text-color: #666666;
/* idea is navbar as Sam's command window */
--nav-bg: #EAFFFF;
--nav-border: #88CCCC;
--nav-active-bg: #9EEEEE;
--nav-a-selected-bg: #9EEEEE;
--nav-a-selected-active-bg: #9EEEEE;
--nav-text-color: #000000;
--nav-text-color-hover: #000000;
--nav-a-bg-hover: #9EEEEE;
--nav-a-selected-border: #88CCCC;
--nav-a-border-hover: #88CCCC;
--button-bg: #FFFFD1;
--button-bg-active: #FFFFE9;
--button-bg-hover: #FFFFE9;
--button-primary-bg: #FFFFD1;
--button-primary-bg-active: #FFFFE9;
--button-primary-bg-hover: #FFFFE9;
--settings-list-item-text: #{$anchor-color};
}
a, a.main-nav-link.svelte-my25xk, .settings-list-item {
text-decoration: underline;
}
a.mention.u-url, a:hover, a:active, a:focus, ::selection {
background: #EEEE9E;
}
a.mention.u-url:hover, a.mention.u-url:active, a.mention.u-url:focus, a::selection, a ::selection {
background: #444400;
color: #EEEE9E;
}
a.status-author-name, a.status-header-author {
text-decoration: none;
border-bottom: solid 1px #444400;
}

View File

@ -35,9 +35,6 @@ 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()
})())
})
@ -49,7 +46,7 @@ self.addEventListener('activate', event => {
// delete old asset/ondemand caches
for (let key of keys) {
if (key !== ASSETS &&
!key.startsWith('webpack_assets_')) {
!key.startsWith('webpack_assets_')) {
await caches.delete(key)
}
}
@ -134,78 +131,86 @@ async function showSimpleNotification (data) {
}
async function showRichNotification (data, notification) {
const { icon, body } = data
const tag = notification.id
const { origin } = self.location
const badge = '/icon-push-badge.png'
const { origin } = new URL(data.icon)
switch (notification.type) {
case 'follow': {
await self.registration.showNotification(data.title, {
badge,
icon,
body,
tag,
icon: data.icon,
body: data.body,
tag: notification.id,
data: {
url: `${origin}/accounts/${notification.account.id}`
url: `${self.location.origin}/accounts/${notification.account.id}`
}
})
break
}
case 'reblog':
case 'favourite':
case 'poll':
await self.registration.showNotification(data.title, {
badge,
icon,
body,
tag,
data: {
url: `${origin}/statuses/${notification.status.id}`
}
})
break
case 'mention':
const isPublic = ['public', 'unlisted'].includes(notification.status.visibility)
const actions = [
isPublic && {
case 'mention': {
const actions = [{
action: 'favourite',
title: 'Favourite'
}]
if ('reply' in NotificationEvent.prototype) {
actions.splice(0, 0, {
action: 'reply',
type: 'text',
title: 'Reply'
})
}
if (['public', 'unlisted'].includes(notification.status.visibility)) {
actions.push({
action: 'reblog',
icon: '/icon-push-fa-retweet.png', // generated manually from font-awesome-svg
title: 'Boost'
},
{
action: 'favourite',
icon: '/icon-push-fa-star.png', // generated manually from font-awesome-svg
title: 'Favorite'
}
].filter(Boolean)
})
}
await self.registration.showNotification(data.title, {
badge,
icon,
body,
tag,
icon: data.icon,
body: data.body,
tag: notification.id,
data: {
instance: new URL(data.icon).origin,
instance: origin,
status_id: notification.status.id,
access_token: data.access_token,
url: `${origin}/statuses/${notification.status.id}`
url: `${self.location.origin}/statuses/${notification.status.id}`
},
actions
})
break
}
case 'reblog': {
await self.registration.showNotification(data.title, {
icon: data.icon,
body: data.body,
tag: notification.id,
data: {
url: `${self.location.origin}/statuses/${notification.status.id}`
}
})
break
}
case 'favourite': {
await self.registration.showNotification(data.title, {
icon: data.icon,
body: data.body,
tag: notification.id,
data: {
url: `${self.location.origin}/statuses/${notification.status.id}`
}
})
break
}
}
}
const cloneNotification = notification => {
const clone = {}
const clone = { }
// Object.assign() does not work with notifications
for (let k in notification) {
// deliberately not doing a hasOwnProperty check, but skipping
// functions and null props like onclick and onshow and showTrigger
if (typeof notification[k] !== 'function' && notification[k] !== null) {
clone[k] = notification[k]
}
clone[k] = notification[k]
}
return clone
@ -222,19 +227,21 @@ const updateNotificationWithoutAction = (notification, action) => {
self.addEventListener('notificationclick', event => {
event.waitUntil((async () => {
switch (event.action) {
case 'reply': {
await post(`${event.notification.data.instance}/api/v1/statuses/`, {
status: event.reply,
in_reply_to_id: event.notification.data.status_id
}, { 'Authorization': `Bearer ${event.notification.data.access_token}` })
await updateNotificationWithoutAction(event.notification, 'reply')
break
}
case 'reblog': {
const url = `${event.notification.data.instance}/api/v1/statuses/${event.notification.data.status_id}/reblog`
await post(url, null, {
'Authorization': `Bearer ${event.notification.data.access_token}`
})
await post(`${event.notification.data.instance}/api/v1/statuses/${event.notification.data.status_id}/reblog`, null, { 'Authorization': `Bearer ${event.notification.data.access_token}` })
await updateNotificationWithoutAction(event.notification, 'reblog')
break
}
case 'favourite': {
const url = `${event.notification.data.instance}/api/v1/statuses/${event.notification.data.status_id}/favourite`
await post(url, null, {
'Authorization': `Bearer ${event.notification.data.access_token}`
})
await post(`${event.notification.data.instance}/api/v1/statuses/${event.notification.data.status_id}/favourite`, null, { 'Authorization': `Bearer ${event.notification.data.access_token}` })
await updateNotificationWithoutAction(event.notification, 'favourite')
break
}
@ -246,11 +253,3 @@ self.addEventListener('notificationclick', event => {
}
})())
})
self.addEventListener('message', (event) => {
switch (event.data) {
case 'skip-waiting':
self.skipWaiting()
break
}
})

BIN
static/apple-icon-120.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 905 B

View File

@ -9,8 +9,6 @@ import { followAccount, unfollowAccount } from '../src/routes/_api/follow'
import { updateCredentials } from '../src/routes/_api/updateCredentials'
import { reblogStatus } from '../src/routes/_api/reblog'
import { submitMedia } from './submitMedia'
import { voteOnPoll } from '../src/routes/_api/polls'
import { POLL_EXPIRY_DEFAULT } from '../src/routes/_static/polls'
global.fetch = fetch
global.File = FileApi.File
@ -70,15 +68,3 @@ export async function unfollowAs (username, userToFollow) {
export async function updateUserDisplayNameAs (username, displayName) {
return updateCredentials(instanceName, users[username].accessToken, { display_name: displayName })
}
export async function createPollAs (username, content, options, multiple) {
return postStatus(instanceName, users[username].accessToken, content, null, null, false, null, 'public', {
options,
multiple,
expires_in: POLL_EXPIRY_DEFAULT
})
}
export async function voteOnPollAs (username, pollId, choices) {
return voteOnPoll(instanceName, users[username].accessToken, pollId, choices.map(_ => _.toString()))
}

View File

@ -1,22 +1,22 @@
import {
getUrl, notificationsTabAll, notificationsTabMentions,
getUrl, notificationFiltersAll, notificationFiltersMention,
notificationsNavButton, validateTimeline
} from '../utils'
import { loginAsFoobar } from '../roles'
import { notificationsMentions, notifications } from '../fixtures'
fixture`033-notification-mentions.js`
fixture`033-notification-filters.js`
.page`http://localhost:4002`
test('Shows notification mentions', async t => {
test('Shows notification filters', async t => {
await loginAsFoobar(t)
await t
.click(notificationsNavButton)
.expect(getUrl()).match(/\/notifications$/)
.click(notificationsTabMentions)
.click(notificationFiltersMention)
.expect(getUrl()).match(/\/notifications\/mentions$/)
await validateTimeline(t, notificationsMentions)
await t.click(notificationsTabAll)
await t.click(notificationFiltersAll)
.expect(getUrl()).match(/\/notifications$/)
await validateTimeline(t, notifications)
})

View File

@ -1,23 +0,0 @@
import {
validateTimeline, settingsNavButton, instanceSettingHomeReblogs, homeNavButton
} from '../utils'
import { loginAsFoobar } from '../roles'
import { homeTimeline } from '../fixtures'
import { Selector as $ } from 'testcafe'
fixture`034-home-timeline-filters.js`
.page`http://localhost:4002`
test('Filters reblogs from home timeline', async t => {
await loginAsFoobar(t)
await t
.click(settingsNavButton)
.click($('a').withText('Instances'))
.click($('a').withText('localhost:3000'))
.click(instanceSettingHomeReblogs)
.expect(instanceSettingHomeReblogs.checked).notOk()
.click(homeNavButton)
await validateTimeline(t, homeTimeline.filter(({ content }) => {
return content !== 'pinned toot 1'
}))
})

View File

@ -1,48 +0,0 @@
import {
validateTimeline,
settingsNavButton,
instanceSettingNotificationReblogs,
notificationsNavButton,
instanceSettingNotificationFavs,
instanceSettingNotificationFollows,
instanceSettingNotificationMentions
} from '../utils'
import { loginAsFoobar } from '../roles'
import { notifications } from '../fixtures'
import { Selector as $ } from 'testcafe'
fixture`035-notification-timeline-filters.js`
.page`http://localhost:4002`
function setSettingAndGoToNotifications (t, setting) {
return t.click(settingsNavButton)
.click($('a').withText('Instances'))
.click($('a').withText('localhost:3000'))
.click(setting)
.expect(setting.checked).notOk()
.click(notificationsNavButton)
}
test('Filters reblogs from notification timeline', async t => {
await loginAsFoobar(t)
await setSettingAndGoToNotifications(t, instanceSettingNotificationReblogs)
await validateTimeline(t, notifications.filter(_ => !_.rebloggedBy))
})
test('Filters favs from notification timeline', async t => {
await loginAsFoobar(t)
await setSettingAndGoToNotifications(t, instanceSettingNotificationFavs)
await validateTimeline(t, notifications.filter(_ => !_.favoritedBy))
})
test('Filters follows from notification timeline', async t => {
await loginAsFoobar(t)
await setSettingAndGoToNotifications(t, instanceSettingNotificationFollows)
await validateTimeline(t, notifications.filter(_ => !_.followedBy))
})
test('Filters mentions from notification timeline', async t => {
await loginAsFoobar(t)
await setSettingAndGoToNotifications(t, instanceSettingNotificationMentions)
await validateTimeline(t, notifications.filter(_ => !_.content))
})

View File

@ -1,39 +0,0 @@
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')
})

View File

@ -1,12 +1,12 @@
import {
getNthStatusContent,
getUrl, notificationsTabAll, notificationsTabMentions,
getUrl, notificationFiltersAll, notificationFiltersMention,
notificationsNavButton, sleep
} from '../utils'
import { loginAsFoobar } from '../roles'
import { favoriteStatusAs, postAs } from '../serverActions'
fixture`123-notification-mentions.js`
fixture`123-notification-filters.js`
.page`http://localhost:4002`
// maybe in the "mentions" view it should prevent the notification icon from showing (1), (2) etc
@ -18,7 +18,7 @@ test('Handles incoming notifications that are mentions', async t => {
await t
.click(notificationsNavButton)
.expect(getUrl()).match(/\/notifications$/)
.click(notificationsTabMentions)
.click(notificationFiltersMention)
.expect(getUrl()).match(/\/notifications\/mentions$/)
await sleep(2000)
await postAs('admin', 'hey @foobar I am mentioning you')
@ -27,7 +27,7 @@ test('Handles incoming notifications that are mentions', async t => {
timeout
})
.expect(getNthStatusContent(1).innerText).contains('hey @foobar I am mentioning you')
.click(notificationsTabAll)
.click(notificationFiltersAll)
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (current page)', { timeout })
})
@ -39,7 +39,7 @@ test('Handles incoming notifications that are not mentions', async t => {
await t
.click(notificationsNavButton)
.expect(getUrl()).match(/\/notifications$/)
.click(notificationsTabMentions)
.click(notificationFiltersMention)
.expect(getUrl()).match(/\/notifications\/mentions$/)
await sleep(2000)
await postAs('admin', 'woot I am mentioning you again @foobar')
@ -57,7 +57,7 @@ test('Handles incoming notifications that are not mentions', async t => {
await sleep(2000)
await t
.expect(getNthStatusContent(1).innerText).contains('woot I am mentioning you again @foobar')
.click(notificationsTabAll)
.click(notificationFiltersAll)
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (current page)', { timeout })
await t
.expect(getNthStatusContent(1).innerText).contains('this is a post that I hope somebody will favorite')

View File

@ -1,29 +0,0 @@
import {
settingsNavButton, instanceSettingHomeReblogs, homeNavButton, sleep, getNthStatusContent
} from '../utils'
import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe'
import { postAs, reblogStatusAs } from '../serverActions'
fixture`124-home-timeline-filters.js`
.page`http://localhost:4002`
test('Filters favs from home timeline', async t => {
await postAs('foobar', 'Nobody should boost this')
await sleep(1000)
let { id: statusId } = await postAs('quux', 'I hope someone cool boosts this')
await reblogStatusAs('admin', statusId)
await sleep(2000)
await loginAsFoobar(t)
await t
.expect(getNthStatusContent(1).innerText).contains('I hope someone cool boosts this')
.expect(getNthStatusContent(2).innerText).contains('Nobody should boost this')
.click(settingsNavButton)
.click($('a').withText('Instances'))
.click($('a').withText('localhost:3000'))
.click(instanceSettingHomeReblogs)
.expect(instanceSettingHomeReblogs.checked).notOk()
.click(homeNavButton)
await t
.expect(getNthStatusContent(1).innerText).contains('Nobody should boost this')
})

View File

@ -1,127 +0,0 @@
import {
settingsNavButton,
getNthStatusContent,
instanceSettingNotificationReblogs,
notificationBadge,
instanceSettingNotificationFavs,
instanceSettingNotificationMentions,
instanceSettingNotificationFollows,
notificationsNavButton,
getUrl,
sleep, showMoreButton, scrollToBottom, scrollToTop
} from '../utils'
import { loginAsFoobar } from '../roles'
import { Selector as $ } from 'testcafe'
import { favoriteStatusAs, followAs, postAs, reblogStatusAs, unfollowAs } from '../serverActions'
fixture`125-notification-timeline-filters.js`
.page`http://localhost:4002`
test('Notification timeline filters correctly affect counts - boosts', async t => {
let timeout = 20000
let { id: statusId } = await postAs('foobar', 'I do not care if you boost this')
await loginAsFoobar(t)
await t
.expect(getNthStatusContent(1).innerText).contains('I do not care if you boost this')
await reblogStatusAs('admin', statusId)
await t
.expect(notificationBadge.innerText).eql('1', { timeout })
.click(settingsNavButton)
.click($('a').withText('Instances'))
.click($('a').withText('localhost:3000'))
.click(instanceSettingNotificationReblogs)
.expect(instanceSettingNotificationReblogs.checked).notOk()
.expect(notificationBadge.exists).notOk({ timeout })
.click(instanceSettingNotificationReblogs)
.expect(instanceSettingNotificationReblogs.checked).ok()
.expect(notificationBadge.innerText).eql('1', { timeout })
})
test('Notification timeline filters correctly affect counts - favs', async t => {
let timeout = 20000
let { id: statusId } = await postAs('foobar', 'I do not care if you fav this')
await loginAsFoobar(t)
await t
.expect(getNthStatusContent(1).innerText).contains('I do not care if you fav this')
await favoriteStatusAs('admin', statusId)
await t
.expect(notificationBadge.innerText).eql('1', { timeout })
.click(settingsNavButton)
.click($('a').withText('Instances'))
.click($('a').withText('localhost:3000'))
.click(instanceSettingNotificationFavs)
.expect(instanceSettingNotificationFavs.checked).notOk()
.expect(notificationBadge.exists).notOk({ timeout })
.click(instanceSettingNotificationFavs)
.expect(instanceSettingNotificationFavs.checked).ok()
.expect(notificationBadge.innerText).eql('1', { timeout })
})
test('Notification timeline filters correctly affect counts - favs', async t => {
let timeout = 20000
await loginAsFoobar(t)
await t
.expect(getNthStatusContent(1).exists).ok()
await postAs('admin', 'hey yo @foobar')
await t
.expect(notificationBadge.innerText).eql('1', { timeout })
.click(settingsNavButton)
.click($('a').withText('Instances'))
.click($('a').withText('localhost:3000'))
.click(instanceSettingNotificationMentions)
.expect(instanceSettingNotificationMentions.checked).notOk()
.expect(notificationBadge.exists).notOk({ timeout })
.click(instanceSettingNotificationMentions)
.expect(instanceSettingNotificationMentions.checked).ok()
.expect(notificationBadge.innerText).eql('1', { timeout })
})
test('Notification timeline filters correctly affect counts - follows', async t => {
let timeout = 20000
await loginAsFoobar(t)
await t
.expect(getNthStatusContent(1).exists).ok()
await followAs('ExternalLinks', 'foobar')
await t
.expect(notificationBadge.innerText).eql('1', { timeout })
.click(settingsNavButton)
.click($('a').withText('Instances'))
.click($('a').withText('localhost:3000'))
.click(instanceSettingNotificationFollows)
.expect(instanceSettingNotificationFollows.checked).notOk()
.expect(notificationBadge.exists).notOk({ timeout })
.click(instanceSettingNotificationFollows)
.expect(instanceSettingNotificationMentions.checked).ok()
.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')
})

View File

@ -1,94 +0,0 @@
import {
getNthStatusContent,
getNthStatusPollOption,
getNthStatusPollVoteButton,
getNthStatusPollForm,
getNthStatusPollResult,
sleep,
getNthStatusPollRefreshButton,
getNthStatusPollVoteCount,
getNthStatusRelativeDate, getUrl, goBack
} from '../utils'
import { loginAsFoobar } from '../roles'
import { createPollAs, voteOnPollAs } from '../serverActions'
fixture`126-polls.js`
.page`http://localhost:4002`
test('Can vote on polls', async t => {
await loginAsFoobar(t)
await createPollAs('admin', 'vote on my cool poll', ['yes', 'no'], false)
await t
.expect(getNthStatusContent(1).innerText).contains('vote on my cool poll')
.expect(getNthStatusPollVoteCount(1).innerText).eql('0 votes')
.click(getNthStatusPollOption(1, 2))
.click(getNthStatusPollVoteButton(1))
.expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 })
.expect(getNthStatusPollResult(1, 1).innerText).eql('0% yes')
.expect(getNthStatusPollResult(1, 2).innerText).eql('100% no')
.expect(getNthStatusPollVoteCount(1).innerText).eql('1 vote')
})
test('Can vote on multiple-choice polls', async t => {
await loginAsFoobar(t)
await createPollAs('admin', 'vote on my other poll', ['yes', 'no', 'maybe'], true)
await t
.expect(getNthStatusContent(1).innerText).contains('vote on my other poll')
.click(getNthStatusPollOption(1, 1))
.click(getNthStatusPollOption(1, 3))
.click(getNthStatusPollVoteButton(1))
.expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 })
.expect(getNthStatusPollResult(1, 1).innerText).eql('50% yes')
.expect(getNthStatusPollResult(1, 2).innerText).eql('0% no')
.expect(getNthStatusPollResult(1, 3).innerText).eql('50% maybe')
.expect(getNthStatusPollVoteCount(1).innerText).eql('2 votes')
})
test('Can update poll results', async t => {
const { poll } = await createPollAs('admin', 'vote on this poll', ['yes', 'no', 'maybe'], false)
const { id: pollId } = poll
await voteOnPollAs('baz', pollId, [1])
await voteOnPollAs('ExternalLinks', pollId, [1])
await voteOnPollAs('foobar', pollId, [2])
await sleep(1000)
await loginAsFoobar(t)
await t
.expect(getNthStatusContent(1).innerText).contains('vote on this poll')
.expect(getNthStatusPollForm(1).exists).notOk()
.expect(getNthStatusPollResult(1, 1).innerText).eql('0% yes')
.expect(getNthStatusPollResult(1, 2).innerText).eql('67% no')
.expect(getNthStatusPollResult(1, 3).innerText).eql('33% maybe')
.expect(getNthStatusPollVoteCount(1).innerText).eql('3 votes')
await sleep(1000)
await voteOnPollAs('quux', pollId, [0])
await sleep(1000)
await t
.click(getNthStatusPollRefreshButton(1))
.expect(getNthStatusPollResult(1, 1).innerText).eql('25% yes', { timeout: 20000 })
.expect(getNthStatusPollResult(1, 2).innerText).eql('50% no')
.expect(getNthStatusPollResult(1, 3).innerText).eql('25% maybe')
.expect(getNthStatusPollVoteCount(1).innerText).eql('4 votes')
})
test('Poll results refresh everywhere', async t => {
await loginAsFoobar(t)
await createPollAs('admin', 'another poll', ['yes', 'no'], false)
await t
.expect(getNthStatusContent(1).innerText).contains('another poll')
.click(getNthStatusRelativeDate(1))
.expect(getUrl()).contains('/statuses')
.expect(getNthStatusContent(1).innerText).contains('another poll')
.click(getNthStatusPollOption(1, 1))
.click(getNthStatusPollVoteButton(1))
.expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 })
.expect(getNthStatusPollResult(1, 1).innerText).eql('100% yes')
.expect(getNthStatusPollResult(1, 2).innerText).eql('0% no')
.expect(getNthStatusPollVoteCount(1).innerText).eql('1 vote')
await goBack()
await t
.expect(getUrl()).eql('http://localhost:4002/')
.expect(getNthStatusPollForm(1).exists).notOk({ timeout: 20000 })
.expect(getNthStatusPollResult(1, 1).innerText).eql('100% yes')
.expect(getNthStatusPollResult(1, 2).innerText).eql('0% no')
.expect(getNthStatusPollVoteCount(1).innerText).eql('1 vote')
})

View File

@ -1,67 +0,0 @@
import {
getNthStatusContent,
getNthStatusPollForm,
getNthStatusPollResult,
getNthStatusPollVoteCount,
pollButton,
getComposePollNthInput,
composePoll,
composePollMultipleChoice,
composePollExpiry, composePollAddButton, getComposePollRemoveNthButton, postStatusButton, composeInput
} from '../utils'
import { loginAsFoobar } from '../roles'
import { POLL_EXPIRY_DEFAULT } from '../../src/routes/_static/polls'
fixture`127-compose-polls.js`
.page`http://localhost:4002`
test('Can add and remove poll', async t => {
await loginAsFoobar(t)
await t
.expect(composePoll.exists).notOk()
.expect(pollButton.getAttribute('aria-label')).eql('Add poll')
.click(pollButton)
.expect(composePoll.exists).ok()
.expect(getComposePollNthInput(1).value).eql('')
.expect(getComposePollNthInput(2).value).eql('')
.expect(getComposePollNthInput(3).exists).notOk()
.expect(getComposePollNthInput(4).exists).notOk()
.expect(composePollMultipleChoice.checked).notOk()
.expect(composePollExpiry.value).eql(POLL_EXPIRY_DEFAULT.toString())
.expect(pollButton.getAttribute('aria-label')).eql('Remove poll')
.click(pollButton)
.expect(composePoll.exists).notOk()
})
test('Can add and remove poll options', async t => {
await loginAsFoobar(t)
await t
.expect(composePoll.exists).notOk()
.expect(pollButton.getAttribute('aria-label')).eql('Add poll')
.click(pollButton)
.expect(composePoll.exists).ok()
.typeText(getComposePollNthInput(1), 'first', { paste: true })
.typeText(getComposePollNthInput(2), 'second', { paste: true })
.click(composePollAddButton)
.typeText(getComposePollNthInput(3), 'third', { paste: true })
.expect(getComposePollNthInput(1).value).eql('first')
.expect(getComposePollNthInput(2).value).eql('second')
.expect(getComposePollNthInput(3).value).eql('third')
.expect(getComposePollNthInput(4).exists).notOk()
.click(getComposePollRemoveNthButton(2))
.expect(getComposePollNthInput(1).value).eql('first')
.expect(getComposePollNthInput(2).value).eql('third')
.expect(getComposePollNthInput(3).exists).notOk()
.expect(getComposePollNthInput(4).exists).notOk()
.click(composePollAddButton)
.typeText(getComposePollNthInput(3), 'fourth', { paste: true })
.typeText(composeInput, 'Vote on my poll!!!', { paste: true })
.click(postStatusButton)
.expect(getNthStatusContent(1).innerText).contains('Vote on my poll!!!')
.expect(getNthStatusPollForm(1).exists).notOk()
.expect(getNthStatusPollResult(1, 1).innerText).eql('0% first')
.expect(getNthStatusPollResult(1, 2).innerText).eql('0% third')
.expect(getNthStatusPollResult(1, 3).innerText).eql('0% fourth')
.expect(getNthStatusPollResult(1, 4).exists).notOk()
.expect(getNthStatusPollVoteCount(1).innerText).eql('0 votes')
})

View File

@ -1,32 +0,0 @@
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')
})

View File

@ -1,36 +0,0 @@
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)')
})

View File

@ -22,9 +22,8 @@ export const composeButton = $('.compose-box-button')
export const composeLengthIndicator = $('.compose-box-length')
export const emojiButton = $('.compose-box-toolbar button:first-child')
export const mediaButton = $('.compose-box-toolbar button:nth-child(2)')
export const pollButton = $('.compose-box-toolbar button:nth-child(3)')
export const postPrivacyButton = $('.compose-box-toolbar button:nth-child(4)')
export const contentWarningButton = $('.compose-box-toolbar button:nth-child(5)')
export const postPrivacyButton = $('.compose-box-toolbar button:nth-child(3)')
export const contentWarningButton = $('.compose-box-toolbar button:nth-child(4)')
export const emailInput = $('input#user_email')
export const passwordInput = $('input#user_password')
export const authorizeInput = $('button[type=submit]:not(.negative)')
@ -48,25 +47,16 @@ 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')
export const composeModalEmojiButton = $('.modal-dialog .compose-box-toolbar button:nth-child(1)')
export const composeModalPostPrivacyButton = $('.modal-dialog .compose-box-toolbar button:nth-child(4)')
export const composePoll = $('.compose-poll')
export const composePollMultipleChoice = $('.compose-poll input[type="checkbox"]')
export const composePollExpiry = $('.compose-poll select')
export const composePollAddButton = $('.compose-poll button:last-of-type')
export const composeModalPostPrivacyButton = $('.modal-dialog .compose-box-toolbar button:nth-child(3)')
export const postPrivacyDialogButtonUnlisted = $('[aria-label="Post privacy dialog"] li:nth-child(2) button')
@ -74,19 +64,11 @@ export const accountProfileFilterStatuses = $('.account-profile-filters li:nth-c
export const accountProfileFilterStatusesAndReplies = $('.account-profile-filters li:nth-child(2)')
export const accountProfileFilterMedia = $('.account-profile-filters li:nth-child(3)')
export const notificationsTabAll = $('.notification-filters li:nth-child(1)')
export const notificationsTabMentions = $('.notification-filters li:nth-child(2)')
export const instanceSettingHomeReblogs = $('#instance-option-homeReblogs')
export const instanceSettingNotificationFollows = $('#instance-option-notificationFollows')
export const instanceSettingNotificationFavs = $('#instance-option-notificationFavs')
export const instanceSettingNotificationReblogs = $('#instance-option-notificationReblogs')
export const instanceSettingNotificationMentions = $('#instance-option-notificationMentions')
export const notificationBadge = $('#main-nav li:nth-child(2) .nav-link-badge')
export const notificationFiltersAll = $('.notification-filters li:nth-child(1)')
export const notificationFiltersMention = $('.notification-filters li:nth-child(2)')
export function getComposeModalNthMediaAltInput (n) {
return $(`.modal-dialog .compose-media:nth-child(${n}) .compose-media-alt textarea`)
return $(`.modal-dialog .compose-media:nth-child(${n}) .compose-media-alt input`)
}
export function getComposeModalNthMediaImg (n) {
@ -123,10 +105,6 @@ 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) {
@ -219,7 +197,7 @@ export const getScrollTop = exec(() => {
})
export function getNthMediaAltInput (n) {
return $(`.compose-box .compose-media:nth-child(${n}) .compose-media-alt textarea`)
return $(`.compose-box .compose-media:nth-child(${n}) .compose-media-alt input`)
}
export function getNthComposeReplyInput (n) {
@ -231,39 +209,7 @@ export function getNthComposeReplyButton (n) {
}
export function getNthPostPrivacyButton (n) {
return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(4)`)
}
export function getNthStatusPollOption (n, i) {
return $(`${getNthStatusSelector(n)} .poll li:nth-child(${i}) input`)
}
export function getNthStatusPollVoteButton (n) {
return $(`${getNthStatusSelector(n)} .poll button`)
}
export function getNthStatusPollForm (n) {
return $(`${getNthStatusSelector(n)} .poll form`)
}
export function getNthStatusPollResult (n, i) {
return $(`${getNthStatusSelector(n)} .poll li:nth-child(${i})`)
}
export function getNthStatusPollRefreshButton (n) {
return $(`${getNthStatusSelector(n)} button.poll-stat`)
}
export function getNthStatusPollVoteCount (n) {
return $(`${getNthStatusSelector(n)} .poll .poll-stat:nth-child(1) .poll-stat-text`)
}
export function getComposePollNthInput (n) {
return $(`.compose-poll input[type="text"]:nth-of-type(${n})`)
}
export function getComposePollRemoveNthButton (n) {
return $(`.compose-poll button:nth-of-type(${n})`)
return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(3)`)
}
export function getNthAutosuggestionResult (n) {
@ -347,11 +293,11 @@ export function getNthReplyContentWarningInput (n) {
}
export function getNthReplyContentWarningButton (n) {
return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(5)`)
return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(4)`)
}
export function getNthReplyPostPrivacyButton (n) {
return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(4)`)
return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(3)`)
}
export function getNthPostPrivacyOptionInDialog (n) {
@ -436,20 +382,16 @@ 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 = start; i < end; i++) {
for (let i = 1; i < n; 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
.expect(getNthStatus(end).exists).ok({ timeout })
.hover(getNthStatus(end))
await t.hover(getNthStatus(n))
}
export async function clickToNotificationsAndBackHome (t) {

1325
yarn.lock

File diff suppressed because it is too large Load Diff