Compare commits
45 Commits
Author | SHA1 | Date |
---|---|---|
Nolan Lawson | 155cb05e39 | |
Nolan Lawson | 5d0e95e759 | |
Nolan Lawson | 58a8772edc | |
Nolan Lawson | d75507bbce | |
Nolan Lawson | 604471a158 | |
Nolan Lawson | f5c7bc790f | |
Nolan Lawson | 74230cfe8e | |
Nolan Lawson | a35f5ee2d9 | |
Nolan Lawson | 27864fc47f | |
Nolan Lawson | fcf64c2169 | |
Nolan Lawson | 45630c185f | |
Nolan Lawson | 44a87dcd9a | |
Nolan Lawson | 8672ade314 | |
Nolan Lawson | fa2eb8fe52 | |
Nolan Lawson | 0de6c3a09f | |
Nolan Lawson | 34e82cbaf2 | |
Nolan Lawson | f1857cb86e | |
Nolan Lawson | 3453b10ffb | |
Nolan Lawson | 8c74d0c7c8 | |
Nolan Lawson | 3a2c56f0fa | |
Nolan Lawson | 164768e6c9 | |
Nolan Lawson | 3a7d6d3552 | |
Nolan Lawson | 12179505e1 | |
Nolan Lawson | 482ee3d3bb | |
Nolan Lawson | 37d3cac7d2 | |
Nolan Lawson | b45868bbfd | |
Nolan Lawson | 6efc28aac8 | |
Nolan Lawson | 0878275ab9 | |
Nolan Lawson | 2c1de66592 | |
Nolan Lawson | 45441d3a9e | |
Nolan Lawson | dac4b493c8 | |
Nolan Lawson | bf640b9b0f | |
Nolan Lawson | 8f477eeccb | |
greenkeeper[bot] | 979bb4815f | |
Nolan Lawson | 12c5b732ae | |
Nolan Lawson | a17948cf99 | |
Nolan Lawson | 92bff6caaa | |
Nolan Lawson | 02689bec93 | |
Nolan Lawson | c18168d913 | |
sgenoud | af955492e8 | |
Nolan Lawson | 692e8b57c3 | |
Nolan Lawson | d92bd2e94b | |
greenkeeper[bot] | 5178650e78 | |
greenkeeper[bot] | 9862858b2e | |
Cătălin Mariș | cdade05315 |
|
@ -11,7 +11,6 @@ const render = promisify(sass.render.bind(sass))
|
||||||
|
|
||||||
const globalScss = path.join(__dirname, '../src/scss/global.scss')
|
const globalScss = path.join(__dirname, '../src/scss/global.scss')
|
||||||
const defaultThemeScss = path.join(__dirname, '../src/scss/themes/_default.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 customScrollbarScss = path.join(__dirname, '../src/scss/custom-scrollbars.scss')
|
||||||
const themesScssDir = path.join(__dirname, '../src/scss/themes')
|
const themesScssDir = path.join(__dirname, '../src/scss/themes')
|
||||||
const assetsDir = path.join(__dirname, '../static')
|
const assetsDir = path.join(__dirname, '../static')
|
||||||
|
@ -22,11 +21,9 @@ async function renderCss (file) {
|
||||||
|
|
||||||
async function compileGlobalSass () {
|
async function compileGlobalSass () {
|
||||||
let mainStyle = (await Promise.all([defaultThemeScss, globalScss].map(renderCss))).join('')
|
let mainStyle = (await Promise.all([defaultThemeScss, globalScss].map(renderCss))).join('')
|
||||||
let offlineStyle = (await renderCss(offlineThemeScss))
|
|
||||||
let scrollbarStyle = (await renderCss(customScrollbarScss))
|
let scrollbarStyle = (await renderCss(customScrollbarScss))
|
||||||
|
|
||||||
return `<style>\n${mainStyle}</style>\n` +
|
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`
|
`<style media="all" id="theScrollbarStyle">\n${scrollbarStyle}</style>\n`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,9 +42,15 @@ module.exports = [
|
||||||
{ id: 'fa-circle-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/circle-o.svg' },
|
{ 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-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-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-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-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-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-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-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' }
|
||||||
]
|
]
|
||||||
|
|
30
package.json
30
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "pinafore",
|
"name": "pinafore",
|
||||||
"description": "Alternative web client for Mastodon",
|
"description": "Alternative web client for Mastodon",
|
||||||
"version": "1.7.0",
|
"version": "1.9.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "standard && standard --plugin html 'src/routes/**/*.html'",
|
"lint": "standard && standard --plugin html 'src/routes/**/*.html'",
|
||||||
"lint-fix": "standard --fix && standard --fix --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"
|
"build-now-json": "node -r esm ./bin/build-now-json.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.4.4",
|
"@babel/core": "^7.4.5",
|
||||||
"@gamestdio/websocket": "^0.3.2",
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
"@webcomponents/custom-elements": "^1.2.4",
|
"@webcomponents/custom-elements": "^1.2.4",
|
||||||
"babel-loader": "^8.0.5",
|
"babel-loader": "^8.0.6",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||||
"cheerio": "^1.0.0-rc.2",
|
"cheerio": "^1.0.0-rc.2",
|
||||||
"child-process-promise": "^2.2.1",
|
"child-process-promise": "^2.2.1",
|
||||||
|
@ -61,9 +61,9 @@
|
||||||
"emoji-regex": "^8.0.0",
|
"emoji-regex": "^8.0.0",
|
||||||
"encoding": "^0.1.12",
|
"encoding": "^0.1.12",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"esm": "^3.2.22",
|
"esm": "^3.2.25",
|
||||||
"events-light": "^1.0.5",
|
"events-light": "^1.0.5",
|
||||||
"express": "^4.16.4",
|
"express": "^4.17.1",
|
||||||
"file-api": "^0.10.4",
|
"file-api": "^0.10.4",
|
||||||
"file-drop-element": "0.2.0",
|
"file-drop-element": "0.2.0",
|
||||||
"form-data": "^2.3.3",
|
"form-data": "^2.3.3",
|
||||||
|
@ -74,7 +74,7 @@
|
||||||
"lodash-es": "^4.17.11",
|
"lodash-es": "^4.17.11",
|
||||||
"lodash-webpack-plugin": "^0.11.5",
|
"lodash-webpack-plugin": "^0.11.5",
|
||||||
"mkdirp": "^0.5.1",
|
"mkdirp": "^0.5.1",
|
||||||
"node-fetch": "^2.5.0",
|
"node-fetch": "^2.6.0",
|
||||||
"node-sass": "^4.12.0",
|
"node-sass": "^4.12.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"p-any": "^2.1.0",
|
"p-any": "^2.1.0",
|
||||||
|
@ -86,28 +86,28 @@
|
||||||
"quick-lru": "^4.0.0",
|
"quick-lru": "^4.0.0",
|
||||||
"remount": "^0.11.0",
|
"remount": "^0.11.0",
|
||||||
"requestidlecallback": "^0.3.0",
|
"requestidlecallback": "^0.3.0",
|
||||||
"rollup": "^1.11.3",
|
"rollup": "^1.12.4",
|
||||||
"rollup-plugin-replace": "^2.2.0",
|
"rollup-plugin-replace": "^2.2.0",
|
||||||
"rollup-plugin-terser": "^4.0.4",
|
"rollup-plugin-terser": "^5.0.0",
|
||||||
"sapper": "nolanlawson/sapper#for-pinafore-14",
|
"sapper": "nolanlawson/sapper#for-pinafore-14",
|
||||||
"stringz": "^1.0.0",
|
"stringz": "^2.0.0",
|
||||||
"svelte": "^2.16.1",
|
"svelte": "^2.16.1",
|
||||||
"svelte-extras": "^2.0.2",
|
"svelte-extras": "^2.0.2",
|
||||||
"svelte-loader": "^2.13.3",
|
"svelte-loader": "^2.13.4",
|
||||||
"svelte-transitions": "^1.2.0",
|
"svelte-transitions": "^1.2.0",
|
||||||
"svgo": "^1.2.2",
|
"svgo": "^1.2.2",
|
||||||
"terser-webpack-plugin": "^1.2.3",
|
"terser-webpack-plugin": "^1.3.0",
|
||||||
"text-encoding": "^0.7.0",
|
"text-encoding": "^0.7.0",
|
||||||
"tiny-queue": "^0.2.1",
|
"tiny-queue": "^0.2.1",
|
||||||
"webpack": "^4.31.0",
|
"webpack": "^4.32.2",
|
||||||
"webpack-bundle-analyzer": "^3.3.2"
|
"webpack-bundle-analyzer": "^3.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"assert": "^1.5.0",
|
"assert": "^2.0.0",
|
||||||
"eslint-plugin-html": "^5.0.3",
|
"eslint-plugin-html": "^5.0.5",
|
||||||
"fake-indexeddb": "^2.1.0",
|
"fake-indexeddb": "^2.1.0",
|
||||||
"mocha": "^6.1.4",
|
"mocha": "^6.1.4",
|
||||||
"now": "^15.2.0",
|
"now": "^15.3.0",
|
||||||
"standard": "^12.0.1",
|
"standard": "^12.0.1",
|
||||||
"testcafe": "^1.1.4"
|
"testcafe": "^1.1.4"
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,14 +10,24 @@
|
||||||
|
|
||||||
<link id='theManifest' rel='manifest' href='/manifest.json' >
|
<link id='theManifest' rel='manifest' href='/manifest.json' >
|
||||||
<link id='theFavicon' rel='icon' type='image/png' href='/favicon.png' >
|
<link id='theFavicon' rel='icon' type='image/png' href='/favicon.png' >
|
||||||
<link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120.png" >
|
<link rel="apple-touch-icon" href="/apple-icon.png" >
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180.png" >
|
|
||||||
<meta name="mobile-web-app-capable" content="yes" >
|
<meta name="mobile-web-app-capable" content="yes" >
|
||||||
<meta name="apple-mobile-web-app-title" content="Pinafore" >
|
<meta name="apple-mobile-web-app-title" content="Pinafore" >
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="white" >
|
<meta name="apple-mobile-web-app-status-bar-style" content="white" >
|
||||||
|
|
||||||
<!-- inline CSS -->
|
<!-- 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>
|
<noscript>
|
||||||
<style>
|
<style>
|
||||||
.hidden-from-ssr {
|
.hidden-from-ssr {
|
||||||
|
|
|
@ -3,16 +3,21 @@
|
||||||
// To allow CSP to work correctly, we also calculate a sha256 hash during
|
// To allow CSP to work correctly, we also calculate a sha256 hash during
|
||||||
// the build process and write it to checksum.js.
|
// the build process and write it to checksum.js.
|
||||||
|
|
||||||
import { testHasLocalStorageOnce } from '../routes/_utils/testStorage'
|
import { INLINE_THEME, DEFAULT_THEME, switchToTheme } from '../routes/_utils/themeEngine'
|
||||||
import { DEFAULT_LIGHT_THEME, DEFAULT_THEME, switchToTheme } from '../routes/_utils/themeEngine'
|
|
||||||
import { basename } from '../routes/_api/utils'
|
import { basename } from '../routes/_api/utils'
|
||||||
import { onUserIsLoggedOut } from '../routes/_actions/onUserIsLoggedOut'
|
import { onUserIsLoggedOut } from '../routes/_actions/onUserIsLoggedOut'
|
||||||
|
import { storeLite } from '../routes/_store/storeLite'
|
||||||
|
|
||||||
window.__themeColors = process.env.THEME_COLORS
|
window.__themeColors = process.env.THEME_COLORS
|
||||||
|
|
||||||
const safeParse = str => (typeof str === 'undefined' || str === 'undefined') ? undefined : JSON.parse(str)
|
const {
|
||||||
const hasLocalStorage = testHasLocalStorageOnce()
|
currentInstance,
|
||||||
const currentInstance = hasLocalStorage && safeParse(localStorage.store_currentInstance)
|
instanceThemes,
|
||||||
|
disableCustomScrollbars,
|
||||||
|
enableGrayscale
|
||||||
|
} = storeLite.get()
|
||||||
|
|
||||||
|
const theme = (instanceThemes && instanceThemes[currentInstance]) || DEFAULT_THEME
|
||||||
|
|
||||||
if (currentInstance) {
|
if (currentInstance) {
|
||||||
// Do prefetch if we're logged in, so we can connect faster to the other origin.
|
// Do prefetch if we're logged in, so we can connect faster to the other origin.
|
||||||
|
@ -26,24 +31,24 @@ if (currentInstance) {
|
||||||
document.head.appendChild(link)
|
document.head.appendChild(link)
|
||||||
}
|
}
|
||||||
|
|
||||||
let theme = (currentInstance &&
|
if (theme !== INLINE_THEME) {
|
||||||
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
|
// switch theme ASAP to minimize flash of default theme
|
||||||
switchToTheme(theme)
|
switchToTheme(theme, enableGrayscale)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasLocalStorage || !currentInstance) {
|
if (enableGrayscale) {
|
||||||
|
document.getElementById('theGrayscaleStyle')
|
||||||
|
.setAttribute('media', 'all') // enables the style
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentInstance) {
|
||||||
// if not logged in, show all these 'hidden-from-ssr' elements
|
// if not logged in, show all these 'hidden-from-ssr' elements
|
||||||
onUserIsLoggedOut()
|
onUserIsLoggedOut()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasLocalStorage && localStorage.store_disableCustomScrollbars === 'true') {
|
if (disableCustomScrollbars) {
|
||||||
// if user has disabled custom scrollbars, remove this style
|
document.getElementById('theScrollbarStyle')
|
||||||
let theScrollbarStyle = document.getElementById('theScrollbarStyle')
|
.setAttribute('media', 'only x') // disables the style
|
||||||
theScrollbarStyle.setAttribute('media', 'only x') // disables the style
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// hack to make the scrollbars rounded only on macOS
|
// hack to make the scrollbars rounded only on macOS
|
||||||
|
|
|
@ -84,7 +84,8 @@ async function registerNewInstance (code) {
|
||||||
instanceThemes: instanceThemes
|
instanceThemes: instanceThemes
|
||||||
})
|
})
|
||||||
store.save()
|
store.save()
|
||||||
switchToTheme(DEFAULT_THEME)
|
let { enableGrayscale } = store.get()
|
||||||
|
switchToTheme(DEFAULT_THEME, enableGrayscale)
|
||||||
// fire off these requests so they're cached
|
// fire off these requests so they're cached
|
||||||
/* no await */ updateVerifyCredentialsForInstance(currentRegisteredInstanceName)
|
/* no await */ updateVerifyCredentialsForInstance(currentRegisteredInstanceName)
|
||||||
/* no await */ updateCustomEmojiForInstance(currentRegisteredInstanceName)
|
/* no await */ updateCustomEmojiForInstance(currentRegisteredInstanceName)
|
||||||
|
|
|
@ -22,7 +22,7 @@ export async function insertHandleForReply (statusId) {
|
||||||
|
|
||||||
export async function postStatus (realm, text, inReplyToId, mediaIds,
|
export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||||
sensitive, spoilerText, visibility,
|
sensitive, spoilerText, visibility,
|
||||||
mediaDescriptions, inReplyToUuid) {
|
mediaDescriptions, inReplyToUuid, poll) {
|
||||||
let { currentInstance, accessToken, online } = store.get()
|
let { currentInstance, accessToken, online } = store.get()
|
||||||
|
|
||||||
if (!online) {
|
if (!online) {
|
||||||
|
@ -41,7 +41,7 @@ export async function postStatus (realm, text, inReplyToId, mediaIds,
|
||||||
return description && putMediaDescription(currentInstance, accessToken, mediaIds[i], description)
|
return description && putMediaDescription(currentInstance, accessToken, mediaIds[i], description)
|
||||||
}))
|
}))
|
||||||
let status = await postStatusToServer(currentInstance, accessToken, text,
|
let status = await postStatusToServer(currentInstance, accessToken, text,
|
||||||
inReplyToId, mediaIds, sensitive, spoilerText, visibility)
|
inReplyToId, mediaIds, sensitive, spoilerText, visibility, poll)
|
||||||
addStatusOrNotification(currentInstance, 'home', status)
|
addStatusOrNotification(currentInstance, 'home', status)
|
||||||
store.clearComposeData(realm)
|
store.clearComposeData(realm)
|
||||||
emit('postedStatus', realm, inReplyToUuid)
|
emit('postedStatus', realm, inReplyToUuid)
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { store } from '../_store/store'
|
||||||
|
|
||||||
|
export function enablePoll (realm) {
|
||||||
|
store.setComposeData(realm, {
|
||||||
|
poll: {
|
||||||
|
options: [
|
||||||
|
'',
|
||||||
|
''
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disablePoll (realm) {
|
||||||
|
store.setComposeData(realm, {
|
||||||
|
poll: null
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { getVerifyCredentials } from '../_api/user'
|
import { getVerifyCredentials } from '../_api/user'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { DEFAULT_THEME, switchToTheme } from '../_utils/themeEngine'
|
import { switchToTheme } from '../_utils/themeEngine'
|
||||||
import { toast } from '../_components/toast/toast'
|
import { toast } from '../_components/toast/toast'
|
||||||
import { goto } from '../../../__sapper__/client'
|
import { goto } from '../../../__sapper__/client'
|
||||||
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
import { cacheFirstUpdateAfter } from '../_utils/sync'
|
||||||
|
@ -14,7 +14,8 @@ export function changeTheme (instanceName, newTheme) {
|
||||||
store.save()
|
store.save()
|
||||||
let { currentInstance } = store.get()
|
let { currentInstance } = store.get()
|
||||||
if (instanceName === currentInstance) {
|
if (instanceName === currentInstance) {
|
||||||
switchToTheme(newTheme)
|
let { enableGrayscale } = store.get()
|
||||||
|
switchToTheme(newTheme, enableGrayscale)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,7 +27,8 @@ export function switchToInstance (instanceName) {
|
||||||
queryInSearch: ''
|
queryInSearch: ''
|
||||||
})
|
})
|
||||||
store.save()
|
store.save()
|
||||||
switchToTheme(instanceThemes[instanceName])
|
let { enableGrayscale } = store.get()
|
||||||
|
switchToTheme(instanceThemes[instanceName], enableGrayscale)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logOutOfInstance (instanceName) {
|
export async function logOutOfInstance (instanceName) {
|
||||||
|
@ -55,7 +57,8 @@ export async function logOutOfInstance (instanceName) {
|
||||||
})
|
})
|
||||||
store.save()
|
store.save()
|
||||||
toast.say(`Logged out of ${instanceName}`)
|
toast.say(`Logged out of ${instanceName}`)
|
||||||
switchToTheme(instanceThemes[newInstance] || DEFAULT_THEME)
|
let { enableGrayscale } = store.get()
|
||||||
|
switchToTheme(instanceThemes[newInstance], enableGrayscale)
|
||||||
/* no await */ database.clearDatabaseForInstance(instanceName)
|
/* no await */ database.clearDatabaseForInstance(instanceName)
|
||||||
goto('/settings/instances')
|
goto('/settings/instances')
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
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 || ''))
|
||||||
|
}
|
||||||
|
}
|
|
@ -114,7 +114,7 @@ export async function setupTimeline () {
|
||||||
stop('setupTimeline')
|
stop('setupTimeline')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchTimelineItemsOnScrollToBottom (instanceName, timelineName) {
|
export async function fetchMoreItemsAtBottomOfTimeline (instanceName, timelineName) {
|
||||||
console.log('setting runningUpdate: true')
|
console.log('setting runningUpdate: true')
|
||||||
store.setForTimeline(instanceName, timelineName, { runningUpdate: true })
|
store.setForTimeline(instanceName, timelineName, { runningUpdate: true })
|
||||||
await fetchTimelineItemsAndPossiblyFallBack()
|
await fetchTimelineItemsAndPossiblyFallBack()
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
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 })
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import { auth, basename } from './utils'
|
||||||
import { DEFAULT_TIMEOUT, get, post, WRITE_TIMEOUT } from '../_utils/ajax'
|
import { DEFAULT_TIMEOUT, get, post, WRITE_TIMEOUT } from '../_utils/ajax'
|
||||||
|
|
||||||
export async function postStatus (instanceName, accessToken, text, inReplyToId, mediaIds,
|
export async function postStatus (instanceName, accessToken, text, inReplyToId, mediaIds,
|
||||||
sensitive, spoilerText, visibility) {
|
sensitive, spoilerText, visibility, poll) {
|
||||||
let url = `${basename(instanceName)}/api/v1/statuses`
|
let url = `${basename(instanceName)}/api/v1/statuses`
|
||||||
|
|
||||||
let body = {
|
let body = {
|
||||||
|
@ -11,7 +11,8 @@ export async function postStatus (instanceName, accessToken, text, inReplyToId,
|
||||||
media_ids: mediaIds,
|
media_ids: mediaIds,
|
||||||
sensitive: sensitive,
|
sensitive: sensitive,
|
||||||
spoiler_text: spoilerText,
|
spoiler_text: spoilerText,
|
||||||
visibility: visibility
|
visibility: visibility,
|
||||||
|
poll: poll
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let key of Object.keys(body)) {
|
for (let key of Object.keys(body)) {
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
<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>
|
|
@ -7,7 +7,7 @@
|
||||||
{#if hidePage}
|
{#if hidePage}
|
||||||
<LoadingPage />
|
<LoadingPage />
|
||||||
{/if}
|
{/if}
|
||||||
<ComposeBox realm="home" hidden={hidePage}/>
|
<LazyComposeBox realm="home" hidden={hidePage}/>
|
||||||
<div class="timeline-home-anchor-container">
|
<div class="timeline-home-anchor-container">
|
||||||
{#if !hidePage && hideTimeline}
|
{#if !hidePage && hideTimeline}
|
||||||
<LoadingPage />
|
<LoadingPage />
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
import LazyTimeline from './timeline/LazyTimeline.html'
|
import LazyTimeline from './timeline/LazyTimeline.html'
|
||||||
import { store } from '../_store/store.js'
|
import { store } from '../_store/store.js'
|
||||||
import LoadingPage from './LoadingPage.html'
|
import LoadingPage from './LoadingPage.html'
|
||||||
import ComposeBox from './compose/ComposeBox.html'
|
import LazyComposeBox from './compose/LazyComposeBox.html'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oncreate () {
|
oncreate () {
|
||||||
|
@ -44,9 +44,9 @@
|
||||||
},
|
},
|
||||||
store: () => store,
|
store: () => store,
|
||||||
components: {
|
components: {
|
||||||
|
LazyComposeBox,
|
||||||
LazyTimeline,
|
LazyTimeline,
|
||||||
LoadingPage,
|
LoadingPage
|
||||||
ComposeBox
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -13,7 +13,13 @@
|
||||||
<ComposeInput {realm} {text} {autoFocus} on:postAction="doPostStatus()" />
|
<ComposeInput {realm} {text} {autoFocus} on:postAction="doPostStatus()" />
|
||||||
<ComposeLengthGauge {length} {overLimit} />
|
<ComposeLengthGauge {length} {overLimit} />
|
||||||
<ComposeAutosuggest {realm} {text} />
|
<ComposeAutosuggest {realm} {text} />
|
||||||
<ComposeToolbar {realm} {postPrivacy} {media} {contentWarningShown} {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} />
|
||||||
<ComposeLengthIndicator {length} {overLimit} />
|
<ComposeLengthIndicator {length} {overLimit} />
|
||||||
<ComposeMedia {realm} {media} />
|
<ComposeMedia {realm} {media} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,6 +44,7 @@
|
||||||
"avatar input input input"
|
"avatar input input input"
|
||||||
"avatar gauge gauge gauge"
|
"avatar gauge gauge gauge"
|
||||||
"avatar autosuggest autosuggest autosuggest"
|
"avatar autosuggest autosuggest autosuggest"
|
||||||
|
"avatar poll poll poll"
|
||||||
"avatar toolbar toolbar length"
|
"avatar toolbar toolbar length"
|
||||||
"avatar media media media";
|
"avatar media media media";
|
||||||
grid-template-columns: min-content minmax(0, max-content) 1fr 1fr;
|
grid-template-columns: min-content minmax(0, max-content) 1fr 1fr;
|
||||||
|
@ -62,6 +69,10 @@
|
||||||
grid-area: cw;
|
grid-area: cw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compose-poll-wrapper {
|
||||||
|
grid-area: poll;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.compose-box {
|
.compose-box {
|
||||||
padding: 10px 10px 0 10px;
|
padding: 10px 10px 0 10px;
|
||||||
|
@ -83,12 +94,14 @@
|
||||||
import ComposeContentWarning from './ComposeContentWarning.html'
|
import ComposeContentWarning from './ComposeContentWarning.html'
|
||||||
import ComposeFileDrop from './ComposeFileDrop.html'
|
import ComposeFileDrop from './ComposeFileDrop.html'
|
||||||
import ComposeAutosuggest from './ComposeAutosuggest.html'
|
import ComposeAutosuggest from './ComposeAutosuggest.html'
|
||||||
|
import ComposePoll from './ComposePoll.html'
|
||||||
import { measureText } from '../../_utils/measureText'
|
import { measureText } from '../../_utils/measureText'
|
||||||
import { POST_PRIVACY_OPTIONS } from '../../_static/statuses'
|
import { POST_PRIVACY_OPTIONS } from '../../_static/statuses'
|
||||||
import { store } from '../../_store/store'
|
import { store } from '../../_store/store'
|
||||||
import { slide } from 'svelte-transitions'
|
import { slide } from '../../_transitions/slide'
|
||||||
import { postStatus, insertHandleForReply, setReplySpoiler, setReplyVisibility } from '../../_actions/compose'
|
import { postStatus, insertHandleForReply, setReplySpoiler, setReplyVisibility } from '../../_actions/compose'
|
||||||
import { classname } from '../../_utils/classname'
|
import { classname } from '../../_utils/classname'
|
||||||
|
import { POLL_EXPIRY_DEFAULT } from '../../_static/polls'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oncreate () {
|
oncreate () {
|
||||||
|
@ -118,7 +131,8 @@
|
||||||
ComposeMedia,
|
ComposeMedia,
|
||||||
ComposeContentWarning,
|
ComposeContentWarning,
|
||||||
ComposeFileDrop,
|
ComposeFileDrop,
|
||||||
ComposeAutosuggest
|
ComposeAutosuggest,
|
||||||
|
ComposePoll
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
size: void 0,
|
size: void 0,
|
||||||
|
@ -144,6 +158,7 @@
|
||||||
composeData: ({ $currentComposeData, realm }) => $currentComposeData[realm] || {},
|
composeData: ({ $currentComposeData, realm }) => $currentComposeData[realm] || {},
|
||||||
text: ({ composeData }) => composeData.text || '',
|
text: ({ composeData }) => composeData.text || '',
|
||||||
media: ({ composeData }) => composeData.media || [],
|
media: ({ composeData }) => composeData.media || [],
|
||||||
|
poll: ({ composeData }) => composeData.poll,
|
||||||
inReplyToId: ({ composeData }) => composeData.inReplyToId,
|
inReplyToId: ({ composeData }) => composeData.inReplyToId,
|
||||||
postPrivacy: ({ postPrivacyKey }) => POST_PRIVACY_OPTIONS.find(_ => _.key === postPrivacyKey),
|
postPrivacy: ({ postPrivacyKey }) => POST_PRIVACY_OPTIONS.find(_ => _.key === postPrivacyKey),
|
||||||
defaultPostPrivacyKey: ({ $currentVerifyCredentials }) => (
|
defaultPostPrivacyKey: ({ $currentVerifyCredentials }) => (
|
||||||
|
@ -172,7 +187,8 @@
|
||||||
realm,
|
realm,
|
||||||
overLimit,
|
overLimit,
|
||||||
inReplyToUuid, // typical replies, using Pinafore-specific uuid
|
inReplyToUuid, // typical replies, using Pinafore-specific uuid
|
||||||
inReplyToId // delete-and-redraft replies, using standard id
|
inReplyToId, // delete-and-redraft replies, using standard id
|
||||||
|
poll
|
||||||
} = this.get()
|
} = this.get()
|
||||||
let sensitive = media.length && !!contentWarning
|
let sensitive = media.length && !!contentWarning
|
||||||
let mediaIds = media.map(_ => _.data.id)
|
let mediaIds = media.map(_ => _.data.id)
|
||||||
|
@ -183,10 +199,25 @@
|
||||||
return // do nothing if invalid
|
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 */
|
/* no await */
|
||||||
postStatus(realm, text, inReplyTo, mediaIds,
|
postStatus(realm, text, inReplyTo, mediaIds,
|
||||||
sensitive, contentWarning, postPrivacyKey,
|
sensitive, contentWarning, postPrivacyKey,
|
||||||
mediaDescriptions, inReplyToUuid)
|
mediaDescriptions, inReplyToUuid, pollToPost)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
.compose-box-length {
|
.compose-box-length {
|
||||||
grid-area: length;
|
grid-area: length;
|
||||||
justify-self: right;
|
justify-self: right;
|
||||||
color: var(--main-theme-color);
|
color: var(--length-indicator-color);
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
@ -53,4 +53,4 @@
|
||||||
observe
|
observe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
{#if media.length}
|
{#if media.length}
|
||||||
<div class="compose-media-container" style="grid-template-columns: repeat({media.length}, 1fr);">
|
<ul class="compose-media-container"
|
||||||
|
aria-label="Media uploads"
|
||||||
|
style="grid-template-columns: repeat({media.length}, 1fr);"
|
||||||
|
>
|
||||||
{#each media as mediaItem, index}
|
{#each media as mediaItem, index}
|
||||||
<ComposeMediaItem {realm} {mediaItem} {index} {media} />
|
<ComposeMediaItem {realm} {mediaItem} {index} {media} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
<style>
|
<style>
|
||||||
.compose-media-container {
|
.compose-media-container {
|
||||||
grid-area: media;
|
grid-area: media;
|
||||||
|
list-style: none;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-column-gap: 5px;
|
grid-column-gap: 5px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-top: 10px;
|
margin: 10px 0 0 0;
|
||||||
background: var(--form-bg);
|
background: var(--form-bg);
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<div class="compose-media compose-media-realm-{realm}">
|
<li class="compose-media compose-media-realm-{realm}">
|
||||||
<img src={mediaItem.data.preview_url} {alt} />
|
<img src={mediaItem.data.preview_url} {alt} />
|
||||||
<div class="compose-media-delete">
|
<div class="compose-media-delete">
|
||||||
<button class="compose-media-delete-button"
|
<button class="compose-media-delete-button"
|
||||||
|
@ -8,19 +8,21 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="compose-media-alt">
|
<div class="compose-media-alt">
|
||||||
<input id="compose-media-input-{uuid}"
|
<textarea id="compose-media-input-{uuid}"
|
||||||
type="text"
|
|
||||||
class="compose-media-alt-input"
|
class="compose-media-alt-input"
|
||||||
placeholder="Description"
|
placeholder="Describe for the visually impaired"
|
||||||
|
ref:textarea
|
||||||
bind:value=rawText
|
bind:value=rawText
|
||||||
>
|
></textarea>
|
||||||
<label for="compose-media-input-{uuid}" class="sr-only">
|
<label for="compose-media-input-{uuid}" class="sr-only">
|
||||||
Describe {shortName} for the visually impaired
|
Describe {shortName} for the visually impaired
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
<style>
|
<style>
|
||||||
.compose-media {
|
.compose-media {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -49,6 +51,9 @@
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
background: var(--alt-input-bg);
|
background: var(--alt-input-bg);
|
||||||
color: var(--body-text-color);
|
color: var(--body-text-color);
|
||||||
|
max-height: 100px;
|
||||||
|
border: 1px solid var(--input-border);
|
||||||
|
resize: none;
|
||||||
}
|
}
|
||||||
.compose-media-alt-input:focus {
|
.compose-media-alt-input:focus {
|
||||||
background: var(--main-bg);
|
background: var(--main-bg);
|
||||||
|
@ -64,12 +69,15 @@
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
}
|
}
|
||||||
.compose-media-delete-button {
|
.compose-media-delete-button {
|
||||||
padding: 10px;
|
padding: 7px 10px 5px;
|
||||||
background: none;
|
background: var(--floating-button-bg);
|
||||||
border: none;
|
border: 1px solid var(--button-border);
|
||||||
}
|
}
|
||||||
.compose-media-delete-button:hover {
|
.compose-media-delete-button:hover {
|
||||||
background: var(--toast-border);
|
background: var(--floating-button-bg-hover);
|
||||||
|
}
|
||||||
|
.compose-media-delete-button:active {
|
||||||
|
background: var(--floating-button-bg-active);
|
||||||
}
|
}
|
||||||
:global(.compose-media-delete-button-svg) {
|
:global(.compose-media-delete-button-svg) {
|
||||||
fill: var(--button-text);
|
fill: var(--button-text);
|
||||||
|
@ -85,6 +93,9 @@
|
||||||
.compose-media-realm-dialog {
|
.compose-media-realm-dialog {
|
||||||
max-height: 15vh;
|
max-height: 15vh;
|
||||||
}
|
}
|
||||||
|
.compose-media-alt-input {
|
||||||
|
max-height: 7vh;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
|
@ -94,11 +105,16 @@
|
||||||
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
import { scheduleIdleTask } from '../../_utils/scheduleIdleTask'
|
||||||
import { observe } from 'svelte-extras'
|
import { observe } from 'svelte-extras'
|
||||||
import SvgIcon from '../SvgIcon.html'
|
import SvgIcon from '../SvgIcon.html'
|
||||||
|
import { autosize } from '../../_thirdparty/autosize/autosize'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oncreate () {
|
oncreate () {
|
||||||
this.setupSyncFromStore()
|
this.setupSyncFromStore()
|
||||||
this.setupSyncToStore()
|
this.setupSyncToStore()
|
||||||
|
this.setupAutosize()
|
||||||
|
},
|
||||||
|
ondestroy () {
|
||||||
|
this.teardownAutosize()
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
rawText: ''
|
rawText: ''
|
||||||
|
@ -139,6 +155,12 @@
|
||||||
saveStore()
|
saveStore()
|
||||||
}, { init: false })
|
}, { init: false })
|
||||||
},
|
},
|
||||||
|
setupAutosize () {
|
||||||
|
autosize(this.refs.textarea)
|
||||||
|
},
|
||||||
|
teardownAutosize () {
|
||||||
|
autosize.destroy(this.refs.textarea)
|
||||||
|
},
|
||||||
onDeleteMedia () {
|
onDeleteMedia () {
|
||||||
let {
|
let {
|
||||||
realm,
|
realm,
|
||||||
|
|
|
@ -0,0 +1,157 @@
|
||||||
|
<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>
|
|
@ -1,11 +1,13 @@
|
||||||
<div class="compose-box-toolbar">
|
<div class="compose-box-toolbar">
|
||||||
<div class="compose-box-toolbar-items">
|
<div class="compose-box-toolbar-items">
|
||||||
<IconButton
|
<IconButton
|
||||||
|
className="compose-toolbar-button"
|
||||||
label="Insert emoji"
|
label="Insert emoji"
|
||||||
href="#fa-smile"
|
href="#fa-smile"
|
||||||
on:click="onEmojiClick()"
|
on:click="onEmojiClick()"
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
className="compose-toolbar-button"
|
||||||
svgClassName={$uploadingMedia ? 'spin' : ''}
|
svgClassName={$uploadingMedia ? 'spin' : ''}
|
||||||
label="Add media"
|
label="Add media"
|
||||||
href={$uploadingMedia ? '#fa-spinner' : '#fa-camera'}
|
href={$uploadingMedia ? '#fa-spinner' : '#fa-camera'}
|
||||||
|
@ -13,11 +15,21 @@
|
||||||
disabled={$uploadingMedia || (media.length === 4)}
|
disabled={$uploadingMedia || (media.length === 4)}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<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})"
|
label="Adjust privacy (currently {postPrivacy.label})"
|
||||||
href={postPrivacy.icon}
|
href={postPrivacy.icon}
|
||||||
on:click="onPostPrivacyClick()"
|
on:click="onPostPrivacyClick()"
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
className="compose-toolbar-button"
|
||||||
label={contentWarningShown ? 'Remove content warning' : 'Add content warning'}
|
label={contentWarningShown ? 'Remove content warning' : 'Add content warning'}
|
||||||
href="#fa-exclamation-triangle"
|
href="#fa-exclamation-triangle"
|
||||||
on:click="onContentWarningClick()"
|
on:click="onContentWarningClick()"
|
||||||
|
@ -40,6 +52,13 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 320px) {
|
||||||
|
:global(button.icon-button.compose-toolbar-button) {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import IconButton from '../IconButton.html'
|
import IconButton from '../IconButton.html'
|
||||||
|
@ -48,6 +67,7 @@
|
||||||
import { doMediaUpload } from '../../_actions/media'
|
import { doMediaUpload } from '../../_actions/media'
|
||||||
import { toggleContentWarningShown } from '../../_actions/contentWarnings'
|
import { toggleContentWarningShown } from '../../_actions/contentWarnings'
|
||||||
import { mediaAccept } from '../../_static/media'
|
import { mediaAccept } from '../../_static/media'
|
||||||
|
import { enablePoll, disablePoll } from '../../_actions/composePoll'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -79,6 +99,14 @@
|
||||||
onContentWarningClick () {
|
onContentWarningClick () {
|
||||||
let { realm } = this.get()
|
let { realm } = this.get()
|
||||||
toggleContentWarningShown(realm)
|
toggleContentWarningShown(realm)
|
||||||
|
},
|
||||||
|
onPollClick () {
|
||||||
|
let { poll, realm } = this.get()
|
||||||
|
if (poll && poll.options && poll.options.length) {
|
||||||
|
disablePoll(realm)
|
||||||
|
} else {
|
||||||
|
enablePoll(realm)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
{#await importComposeBox}
|
||||||
|
<!-- awaiting promise -->
|
||||||
|
{:then ComposeBox}
|
||||||
|
<svelte:component this={ComposeBox} {realm} {hidden} />
|
||||||
|
{:catch error}
|
||||||
|
<div>Component failed to load. Try refreshing! {error}</div>
|
||||||
|
{/await}
|
||||||
|
<script>
|
||||||
|
import { importComposeBox } from '../../_utils/asyncModules'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
importComposeBox: importComposeBox()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -29,4 +29,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -127,7 +127,12 @@
|
||||||
numFollowers: ({ account }) => account.followers_count || 0,
|
numFollowers: ({ account }) => account.followers_count || 0,
|
||||||
numStatusesDisplay: ({ numStatuses }) => numberFormat.format(numStatuses),
|
numStatusesDisplay: ({ numStatuses }) => numberFormat.format(numStatuses),
|
||||||
numFollowingDisplay: ({ numFollowing }) => numberFormat.format(numFollowing),
|
numFollowingDisplay: ({ numFollowing }) => numberFormat.format(numFollowing),
|
||||||
numFollowersDisplay: ({ numFollowers }) => numberFormat.format(numFollowers),
|
numFollowersDisplay: ({ numFollowers, $disableFollowerCounts }) => {
|
||||||
|
if ($disableFollowerCounts && numFollowers >= 10) {
|
||||||
|
return '10+'
|
||||||
|
}
|
||||||
|
return numberFormat.format(numFollowers)
|
||||||
|
},
|
||||||
followersLabel: ({ numFollowers }) => `Followed by ${numFollowers}`,
|
followersLabel: ({ numFollowers }) => `Followed by ${numFollowers}`,
|
||||||
followingLabel: ({ numFollowing }) => `Follows ${numFollowing}`
|
followingLabel: ({ numFollowing }) => `Follows ${numFollowing}`
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,29 @@
|
||||||
|
<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>
|
|
@ -0,0 +1,50 @@
|
||||||
|
<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>
|
|
@ -4,18 +4,21 @@
|
||||||
{:elseif $notificationPermission === "denied"}
|
{:elseif $notificationPermission === "denied"}
|
||||||
<p role="alert">You have denied permission to show notifications.</p>
|
<p role="alert">You have denied permission to show notifications.</p>
|
||||||
{/if}
|
{/if}
|
||||||
<form id="push-notification-settings" disabled="{!pushNotificationsSupport}" ref:pushNotificationsForm aria-label="Push notification settings">
|
<form id="push-notification-settings"
|
||||||
<input type="checkbox" id="push-notifications-follow" name="follow" disabled="{!pushNotificationsSupport}" on:change="onPushSettingsChange(event)">
|
disabled="{!pushNotificationsSupport}"
|
||||||
<label for="push-notifications-follow">New followers</label>
|
ref:form
|
||||||
<br>
|
aria-label="Push notification settings">
|
||||||
<input type="checkbox" id="push-notifications-favourite" name="favourite" disabled="{!pushNotificationsSupport}" on:change="onPushSettingsChange(event)">
|
{#each options as option, i (option.key)}
|
||||||
<label for="push-notifications-favourite">Favourites</label>
|
{#if i > 0}
|
||||||
<br>
|
<br>
|
||||||
<input type="checkbox" id="push-notifications-reblog" name="reblog" disabled="{!pushNotificationsSupport}" on:change="onPushSettingsChange(event)">
|
{/if}
|
||||||
<label for="push-notifications-reblog">Boosts</label>
|
<input type="checkbox"
|
||||||
<br>
|
id="push-notifications-{option.key}"
|
||||||
<input type="checkbox" id="push-notifications-mention" name="mention" disabled="{!pushNotificationsSupport}" on:change="onPushSettingsChange(event)">
|
name="{option.key}"
|
||||||
<label for="push-notifications-mention">Mentions</label>
|
disabled="{!pushNotificationsSupport}"
|
||||||
|
on:change="onPushSettingsChange(event)">
|
||||||
|
<label for="push-notifications-{option.key}">{option.label}</label>
|
||||||
|
{/each}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
|
@ -27,11 +30,11 @@
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
line-height: 2em;
|
line-height: 2em;
|
||||||
}
|
}
|
||||||
.push-notifications form[disabled="true"] {
|
form[disabled="true"] {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
.push-notifications p {
|
p {
|
||||||
margin: 0;
|
margin: 0 0 10px 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
|
@ -40,33 +43,56 @@
|
||||||
import { logOutOfInstance } from '../../../_actions/instances'
|
import { logOutOfInstance } from '../../../_actions/instances'
|
||||||
import { updatePushSubscriptionForInstance, updateAlerts } from '../../../_actions/pushSubscription'
|
import { updatePushSubscriptionForInstance, updateAlerts } from '../../../_actions/pushSubscription'
|
||||||
import { toast } from '../../toast/toast'
|
import { toast } from '../../toast/toast'
|
||||||
|
import { get } from '../../../_utils/lodash-lite'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async oncreate () {
|
async oncreate () {
|
||||||
let { instanceName } = this.get()
|
let { instanceName, options } = this.get()
|
||||||
await updatePushSubscriptionForInstance(instanceName)
|
await updatePushSubscriptionForInstance(instanceName)
|
||||||
|
|
||||||
const form = this.refs.pushNotificationsForm
|
const { form } = this.refs
|
||||||
const { pushSubscription } = this.store.get()
|
const { pushSubscription } = this.store.get()
|
||||||
|
|
||||||
form.elements.follow.checked = pushSubscription && pushSubscription.alerts && pushSubscription.alerts.follow
|
for (let { key } of options) {
|
||||||
form.elements.favourite.checked = pushSubscription && pushSubscription.alerts && pushSubscription.alerts.favourite
|
form.elements[key].checked = get(pushSubscription, ['alerts', key])
|
||||||
form.elements.reblog.checked = pushSubscription && pushSubscription.alerts && pushSubscription.alerts.reblog
|
}
|
||||||
form.elements.mention.checked = pushSubscription && pushSubscription.alerts && pushSubscription.alerts.mention
|
|
||||||
},
|
},
|
||||||
store: () => store,
|
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: {
|
computed: {
|
||||||
pushNotificationsSupport: ({ $pushNotificationsSupport }) => $pushNotificationsSupport
|
pushNotificationsSupport: ({ $pushNotificationsSupport }) => $pushNotificationsSupport
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async onPushSettingsChange (e) {
|
async onPushSettingsChange (e) {
|
||||||
const { instanceName } = this.get()
|
const { instanceName, options } = this.get()
|
||||||
const form = this.refs.pushNotificationsForm
|
const { form } = this.refs
|
||||||
const alerts = {
|
const alerts = {}
|
||||||
follow: form.elements.follow.checked,
|
|
||||||
favourite: form.elements.favourite.checked,
|
for (let { key } of options) {
|
||||||
reblog: form.elements.reblog.checked,
|
alerts[key] = form.elements[key].checked
|
||||||
mention: form.elements.mention.checked
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -13,11 +13,11 @@
|
||||||
<StatusAuthorName {...params} />
|
<StatusAuthorName {...params} />
|
||||||
<StatusAuthorHandle {...params} />
|
<StatusAuthorHandle {...params} />
|
||||||
{#if !isStatusInOwnThread}
|
{#if !isStatusInOwnThread}
|
||||||
<StatusRelativeDate {...params} />
|
<StatusRelativeDate {...params} {...timestampParams} />
|
||||||
{/if}
|
{/if}
|
||||||
<StatusSidebar {...params} />
|
<StatusSidebar {...params} />
|
||||||
{#if spoilerText}
|
{#if spoilerText}
|
||||||
<StatusSpoiler {...params} on:recalculateHeight />
|
<StatusSpoiler {...params} {spoilerShown} on:recalculateHeight />
|
||||||
{/if}
|
{/if}
|
||||||
{#if !showContent}
|
{#if !showContent}
|
||||||
<StatusMentions {...params} />
|
<StatusMentions {...params} />
|
||||||
|
@ -31,10 +31,13 @@
|
||||||
{#if showMedia }
|
{#if showMedia }
|
||||||
<StatusMediaAttachments {...params} on:recalculateHeight />
|
<StatusMediaAttachments {...params} on:recalculateHeight />
|
||||||
{/if}
|
{/if}
|
||||||
{#if isStatusInOwnThread}
|
{#if showPoll}
|
||||||
<StatusDetails {...params} />
|
<StatusPoll {...params} />
|
||||||
{/if}
|
{/if}
|
||||||
<StatusToolbar {...params} on:recalculateHeight />
|
{#if isStatusInOwnThread}
|
||||||
|
<StatusDetails {...params} {...timestampParams} />
|
||||||
|
{/if}
|
||||||
|
<StatusToolbar {...params} {replyShown} on:recalculateHeight />
|
||||||
{#if replyShown}
|
{#if replyShown}
|
||||||
<StatusComposeBox {...params} on:recalculateHeight />
|
<StatusComposeBox {...params} on:recalculateHeight />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -58,6 +61,7 @@
|
||||||
"sidebar content content content"
|
"sidebar content content content"
|
||||||
"sidebar card card card"
|
"sidebar card card card"
|
||||||
"sidebar media-grp media-grp media-grp"
|
"sidebar media-grp media-grp media-grp"
|
||||||
|
"sidebar poll poll poll"
|
||||||
"media media media media"
|
"media media media media"
|
||||||
"....... toolbar toolbar toolbar"
|
"....... toolbar toolbar toolbar"
|
||||||
"compose compose compose compose";
|
"compose compose compose compose";
|
||||||
|
@ -92,6 +96,7 @@
|
||||||
"card card"
|
"card card"
|
||||||
"media-grp media-grp"
|
"media-grp media-grp"
|
||||||
"media media"
|
"media media"
|
||||||
|
"poll poll"
|
||||||
"details details"
|
"details details"
|
||||||
"toolbar toolbar"
|
"toolbar toolbar"
|
||||||
"compose compose";
|
"compose compose";
|
||||||
|
@ -119,6 +124,7 @@
|
||||||
import StatusSpoiler from './StatusSpoiler.html'
|
import StatusSpoiler from './StatusSpoiler.html'
|
||||||
import StatusComposeBox from './StatusComposeBox.html'
|
import StatusComposeBox from './StatusComposeBox.html'
|
||||||
import StatusMentions from './StatusMentions.html'
|
import StatusMentions from './StatusMentions.html'
|
||||||
|
import StatusPoll from './StatusPoll.html'
|
||||||
import Shortcut from '../shortcut/Shortcut.html'
|
import Shortcut from '../shortcut/Shortcut.html'
|
||||||
import { store } from '../../_store/store'
|
import { store } from '../../_store/store'
|
||||||
import { goto } from '../../../../__sapper__/client'
|
import { goto } from '../../../../__sapper__/client'
|
||||||
|
@ -138,7 +144,7 @@
|
||||||
import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid'
|
import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid'
|
||||||
import { statusHtmlToPlainText } from '../../_utils/statusHtmlToPlainText'
|
import { statusHtmlToPlainText } from '../../_utils/statusHtmlToPlainText'
|
||||||
|
|
||||||
const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea'])
|
const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea', 'label'])
|
||||||
const isUserInputElement = node => INPUT_TAGS.has(node.localName)
|
const isUserInputElement = node => INPUT_TAGS.has(node.localName)
|
||||||
const isToolbar = node => node.classList.contains('status-toolbar')
|
const isToolbar = node => node.classList.contains('status-toolbar')
|
||||||
const isStatusArticle = node => node.classList.contains('status-article')
|
const isStatusArticle = node => node.classList.contains('status-article')
|
||||||
|
@ -170,6 +176,7 @@
|
||||||
StatusMediaAttachments,
|
StatusMediaAttachments,
|
||||||
StatusContent,
|
StatusContent,
|
||||||
StatusCard,
|
StatusCard,
|
||||||
|
StatusPoll,
|
||||||
StatusSpoiler,
|
StatusSpoiler,
|
||||||
StatusComposeBox,
|
StatusComposeBox,
|
||||||
StatusMentions,
|
StatusMentions,
|
||||||
|
@ -261,19 +268,24 @@
|
||||||
originalStatus.card &&
|
originalStatus.card &&
|
||||||
originalStatus.card.title
|
originalStatus.card.title
|
||||||
),
|
),
|
||||||
|
showPoll: ({ originalStatus }) => (
|
||||||
|
originalStatus.poll
|
||||||
|
),
|
||||||
showMedia: ({ originalStatus, isStatusInNotification }) => (
|
showMedia: ({ originalStatus, isStatusInNotification }) => (
|
||||||
!isStatusInNotification &&
|
!isStatusInNotification &&
|
||||||
originalStatus.media_attachments &&
|
originalStatus.media_attachments &&
|
||||||
originalStatus.media_attachments.length
|
originalStatus.media_attachments.length
|
||||||
),
|
),
|
||||||
originalAccountEmojis: ({ originalAccount }) => (originalAccount.emojis || []),
|
originalAccountEmojis: ({ originalAccount }) => (originalAccount.emojis || []),
|
||||||
|
originalStatusEmojis: ({ originalStatus }) => (originalStatus.emojis || []),
|
||||||
originalAccountDisplayName: ({ originalAccount }) => (originalAccount.display_name || originalAccount.username),
|
originalAccountDisplayName: ({ originalAccount }) => (originalAccount.display_name || originalAccount.username),
|
||||||
originalAccountAccessibleName: ({ originalAccount, $omitEmojiInDisplayNames }) => {
|
originalAccountAccessibleName: ({ originalAccount, $omitEmojiInDisplayNames }) => {
|
||||||
return getAccountAccessibleName(originalAccount, $omitEmojiInDisplayNames)
|
return getAccountAccessibleName(originalAccount, $omitEmojiInDisplayNames)
|
||||||
},
|
},
|
||||||
createdAtDate: ({ originalStatus }) => originalStatus.created_at,
|
createdAtDate: ({ originalStatus }) => originalStatus.created_at,
|
||||||
absoluteFormattedDate: ({ createdAtDate }) => absoluteDateFormatter.format(new Date(createdAtDate)),
|
createdAtDateTS: ({ createdAtDate }) => new Date(createdAtDate).getTime(),
|
||||||
timeagoFormattedDate: ({ createdAtDate }) => formatTimeagoDate(createdAtDate),
|
absoluteFormattedDate: ({ createdAtDateTS }) => absoluteDateFormatter.format(createdAtDateTS),
|
||||||
|
timeagoFormattedDate: ({ createdAtDateTS, $now }) => formatTimeagoDate(createdAtDateTS, $now),
|
||||||
reblog: ({ status }) => status.reblog,
|
reblog: ({ status }) => status.reblog,
|
||||||
ariaLabel: ({ originalAccount, account, plainTextContent, timeagoFormattedDate, spoilerText,
|
ariaLabel: ({ originalAccount, account, plainTextContent, timeagoFormattedDate, spoilerText,
|
||||||
showContent, reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels }) => (
|
showContent, reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels }) => (
|
||||||
|
@ -282,7 +294,7 @@
|
||||||
reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels)
|
reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels)
|
||||||
),
|
),
|
||||||
showHeader: ({ notification, status, timelineType }) => (
|
showHeader: ({ notification, status, timelineType }) => (
|
||||||
(notification && (notification.type === 'reblog' || notification.type === 'favourite')) ||
|
(notification && ['reblog', 'favourite', 'poll'].includes(notification.type)) ||
|
||||||
status.reblog ||
|
status.reblog ||
|
||||||
timelineType === 'pinned'
|
timelineType === 'pinned'
|
||||||
),
|
),
|
||||||
|
@ -293,15 +305,26 @@
|
||||||
timelineType !== 'search' && 'status-in-timeline',
|
timelineType !== 'search' && 'status-in-timeline',
|
||||||
isStatusInOwnThread && 'status-in-own-thread',
|
isStatusInOwnThread && 'status-in-own-thread',
|
||||||
$underlineLinks && 'underline-links',
|
$underlineLinks && 'underline-links',
|
||||||
!$disableTapOnStatus && 'tap-on-status'
|
!$disableTapOnStatus && !isStatusInOwnThread && 'tap-on-status'
|
||||||
)),
|
)),
|
||||||
content: ({ originalStatus }) => originalStatus.content || '',
|
content: ({ originalStatus }) => originalStatus.content || '',
|
||||||
showContent: ({ spoilerText, spoilerShown }) => !spoilerText || spoilerShown,
|
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,
|
params: ({ notification, notificationId, status, statusId, timelineType,
|
||||||
account, accountId, uuid, isStatusInNotification, isStatusInOwnThread,
|
account, accountId, uuid, isStatusInNotification, isStatusInOwnThread,
|
||||||
originalAccount, originalAccountId, spoilerShown, visibility, replyShown,
|
originalAccount, originalAccountId, visibility,
|
||||||
replyVisibility, spoilerText, originalStatus, originalStatusId, inReplyToId,
|
replyVisibility, spoilerText, originalStatus, originalStatusId, inReplyToId,
|
||||||
createdAtDate, timeagoFormattedDate, enableShortcuts, absoluteFormattedDate, shortcutScope }) => ({
|
enableShortcuts, shortcutScope, originalStatusEmojis }) => ({
|
||||||
notification,
|
notification,
|
||||||
notificationId,
|
notificationId,
|
||||||
status,
|
status,
|
||||||
|
@ -314,19 +337,15 @@
|
||||||
isStatusInOwnThread,
|
isStatusInOwnThread,
|
||||||
originalAccount,
|
originalAccount,
|
||||||
originalAccountId,
|
originalAccountId,
|
||||||
spoilerShown,
|
|
||||||
visibility,
|
visibility,
|
||||||
replyShown,
|
|
||||||
replyVisibility,
|
replyVisibility,
|
||||||
spoilerText,
|
spoilerText,
|
||||||
originalStatus,
|
originalStatus,
|
||||||
originalStatusId,
|
originalStatusId,
|
||||||
inReplyToId,
|
inReplyToId,
|
||||||
createdAtDate,
|
|
||||||
timeagoFormattedDate,
|
|
||||||
enableShortcuts,
|
enableShortcuts,
|
||||||
absoluteFormattedDate,
|
shortcutScope,
|
||||||
shortcutScope
|
originalStatusEmojis
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
events: {
|
events: {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<a ref:cardlink href={url} class="status-card" target="_blank" rel="noopener noreferrer">
|
<a ref:cardlink href={url} class="status-card" target="_blank" rel="noopener noreferrer">
|
||||||
<strong class="card-title">
|
<strong class="card-title">
|
||||||
{title}
|
{unescapedTitle}
|
||||||
</strong>
|
</strong>
|
||||||
{#if description}
|
{#if description}
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="card-description">
|
<span class="card-description">
|
||||||
{description}
|
{unescapedDescription}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -87,6 +87,7 @@
|
||||||
<script>
|
<script>
|
||||||
import LazyImage from '../LazyImage.html'
|
import LazyImage from '../LazyImage.html'
|
||||||
import Shortcut from '../shortcut/Shortcut.html'
|
import Shortcut from '../shortcut/Shortcut.html'
|
||||||
|
import { unescape } from '../../_thirdparty/unescape/unescape'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -96,9 +97,11 @@
|
||||||
computed: {
|
computed: {
|
||||||
card: ({ originalStatus }) => originalStatus.card,
|
card: ({ originalStatus }) => originalStatus.card,
|
||||||
title: ({ card }) => card.title,
|
title: ({ card }) => card.title,
|
||||||
|
unescapedTitle: ({ title }) => title && unescape(title),
|
||||||
url: ({ card }) => card.url,
|
url: ({ card }) => card.url,
|
||||||
hostname: ({ url }) => window.URL ? new window.URL(url).hostname : '',
|
hostname: ({ url }) => window.URL ? new window.URL(url).hostname : '',
|
||||||
description: ({ card, hostname }) => card.description || card.provider_name || hostname,
|
description: ({ card, hostname }) => card.description || card.provider_name || hostname,
|
||||||
|
unescapedDescription: ({ description }) => description && unescape(description),
|
||||||
imageUrl: ({ card }) => card.image
|
imageUrl: ({ card }) => card.image
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -76,8 +76,9 @@
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
content: ({ originalStatus }) => (originalStatus.content || ''),
|
content: ({ originalStatus }) => (originalStatus.content || ''),
|
||||||
emojis: ({ originalStatus }) => originalStatus.emojis,
|
massagedContent: ({ content, originalStatusEmojis, $autoplayGifs }) => (
|
||||||
massagedContent: ({ content, emojis, $autoplayGifs }) => massageUserText(content, emojis, $autoplayGifs)
|
massageUserText(content, originalStatusEmojis, $autoplayGifs)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
hydrateContent () {
|
hydrateContent () {
|
||||||
|
|
|
@ -158,29 +158,40 @@
|
||||||
application: ({ originalStatus }) => originalStatus.application,
|
application: ({ originalStatus }) => originalStatus.application,
|
||||||
applicationName: ({ application }) => (application && application.name),
|
applicationName: ({ application }) => (application && application.name),
|
||||||
applicationWebsite: ({ application }) => (application && application.website),
|
applicationWebsite: ({ application }) => (application && application.website),
|
||||||
createdAtDate: ({ originalStatus }) => originalStatus.created_at,
|
numReblogs: ({ $disableReblogCounts, overrideNumReblogs, originalStatus }) => {
|
||||||
numReblogs: ({ overrideNumReblogs, originalStatus }) => {
|
if ($disableReblogCounts) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
if (typeof overrideNumReblogs === 'number') {
|
if (typeof overrideNumReblogs === 'number') {
|
||||||
return overrideNumReblogs
|
return overrideNumReblogs
|
||||||
}
|
}
|
||||||
return originalStatus.reblogs_count || 0
|
return originalStatus.reblogs_count || 0
|
||||||
},
|
},
|
||||||
numFavs: ({ overrideNumFavs, originalStatus }) => {
|
numFavs: ({ $disableFavCounts, overrideNumFavs, originalStatus }) => {
|
||||||
|
if ($disableFavCounts) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
if (typeof overrideNumFavs === 'number') {
|
if (typeof overrideNumFavs === 'number') {
|
||||||
return overrideNumFavs
|
return overrideNumFavs
|
||||||
}
|
}
|
||||||
return originalStatus.favourites_count || 0
|
return originalStatus.favourites_count || 0
|
||||||
},
|
},
|
||||||
displayAbsoluteFormattedDate: ({ createdAtDate, $isMobileSize }) => (
|
displayAbsoluteFormattedDate: ({ createdAtDateTS, $isMobileSize }) => (
|
||||||
$isMobileSize ? shortAbsoluteDateFormatter : absoluteDateFormatter).format(new Date(createdAtDate)
|
($isMobileSize ? shortAbsoluteDateFormatter : absoluteDateFormatter).format(createdAtDateTS)
|
||||||
),
|
),
|
||||||
reblogsLabel: ({ numReblogs }) => {
|
reblogsLabel: ({ $disableReblogCounts, numReblogs }) => {
|
||||||
|
if ($disableReblogCounts) {
|
||||||
|
return 'Boost counts hidden'
|
||||||
|
}
|
||||||
// TODO: intl
|
// TODO: intl
|
||||||
return numReblogs === 1
|
return numReblogs === 1
|
||||||
? `Boosted ${numReblogs} time`
|
? `Boosted ${numReblogs} time`
|
||||||
: `Boosted ${numReblogs} times`
|
: `Boosted ${numReblogs} times`
|
||||||
},
|
},
|
||||||
favoritesLabel: ({ numFavs }) => {
|
favoritesLabel: ({ $disableFavCounts, numFavs }) => {
|
||||||
|
if ($disableFavCounts) {
|
||||||
|
return 'Favorite counts hidden'
|
||||||
|
}
|
||||||
// TODO: intl
|
// TODO: intl
|
||||||
return numFavs === 1
|
return numFavs === 1
|
||||||
? `Favorited ${numFavs} time`
|
? `Favorited ${numFavs} time`
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="status-header {isStatusInNotification ? 'status-in-notification' : ''} {notification && notification.type === 'follow' ? 'header-is-follow' : ''}">
|
<div class="status-header {isStatusInNotification ? 'status-in-notification' : ''} {notificationType === 'follow' ? 'header-is-follow' : ''}">
|
||||||
<div class="status-header-avatar {timelineType === 'pinned' ? 'hidden' : ''}">
|
<div class="status-header-avatar {timelineType === 'pinned' || notificationType === 'poll' ? 'hidden' : ''}">
|
||||||
<Avatar {account} size="extra-small"/>
|
<Avatar {account} size="extra-small"/>
|
||||||
</div>
|
</div>
|
||||||
<SvgIcon className="status-header-svg" href={icon} />
|
<SvgIcon className="status-header-svg" href={icon} />
|
||||||
|
@ -9,7 +9,7 @@
|
||||||
<span class="status-header-author">
|
<span class="status-header-author">
|
||||||
Pinned toot
|
Pinned toot
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:elseif notificationType !== 'poll'}
|
||||||
<a id={elementId}
|
<a id={elementId}
|
||||||
href="/accounts/{accountId}"
|
href="/accounts/{accountId}"
|
||||||
rel="prefetch"
|
rel="prefetch"
|
||||||
|
@ -20,17 +20,7 @@
|
||||||
</a>
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<span class="status-header-action">
|
<span class="status-header-action">{actionText}</span>
|
||||||
{#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>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
|
@ -105,6 +95,7 @@
|
||||||
import Avatar from '../Avatar.html'
|
import Avatar from '../Avatar.html'
|
||||||
import AccountDisplayName from '../profile/AccountDisplayName.html'
|
import AccountDisplayName from '../profile/AccountDisplayName.html'
|
||||||
import SvgIcon from '../SvgIcon.html'
|
import SvgIcon from '../SvgIcon.html'
|
||||||
|
import { store } from '../../_store/store'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -112,17 +103,40 @@
|
||||||
AccountDisplayName,
|
AccountDisplayName,
|
||||||
SvgIcon
|
SvgIcon
|
||||||
},
|
},
|
||||||
|
store: () => store,
|
||||||
computed: {
|
computed: {
|
||||||
elementId: ({ uuid }) => `status-header-${uuid}`,
|
elementId: ({ uuid }) => `status-header-${uuid}`,
|
||||||
icon: ({ notification, status, timelineType }) => {
|
notificationType: ({ notification }) => notification && notification.type,
|
||||||
|
icon: ({ notificationType, status, timelineType }) => {
|
||||||
if (timelineType === 'pinned') {
|
if (timelineType === 'pinned') {
|
||||||
return '#fa-thumb-tack'
|
return '#fa-thumb-tack'
|
||||||
} else if ((notification && notification.type === 'reblog') || (status && status.reblog)) {
|
} else if ((notificationType === 'reblog') || (status && status.reblog)) {
|
||||||
return '#fa-retweet'
|
return '#fa-retweet'
|
||||||
} else if (notification && notification.type === 'follow') {
|
} else if (notificationType === 'follow') {
|
||||||
return '#fa-user-plus'
|
return '#fa-user-plus'
|
||||||
|
} else if (notificationType === 'poll') {
|
||||||
|
return '#fa-bar-chart'
|
||||||
}
|
}
|
||||||
return '#fa-star'
|
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 ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,327 @@
|
||||||
|
<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>
|
|
@ -64,10 +64,9 @@
|
||||||
Shortcut
|
Shortcut
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
emojis: ({ originalStatus }) => originalStatus.emojis,
|
massagedSpoilerText: ({ spoilerText, originalStatusEmojis, $autoplayGifs }) => {
|
||||||
massagedSpoilerText: ({ spoilerText, emojis, $autoplayGifs }) => {
|
|
||||||
spoilerText = escapeHtml(spoilerText)
|
spoilerText = escapeHtml(spoilerText)
|
||||||
return emojifyText(spoilerText, emojis, $autoplayGifs)
|
return emojifyText(spoilerText, originalStatusEmojis, $autoplayGifs)
|
||||||
},
|
},
|
||||||
elementId: ({ uuid }) => `spoiler-${uuid}`
|
elementId: ({ uuid }) => `spoiler-${uuid}`
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,32 +1,100 @@
|
||||||
<div class="loading-footer {shown ? '' : 'hidden'}">
|
<div class="loading-footer {shown ? '' : 'hidden'}">
|
||||||
<LoadingSpinner size={48} />
|
<div class="loading-wrapper {showLoading ? 'shown' : ''}"
|
||||||
<span class="loading-footer-info">
|
aria-hidden={!showLoading}
|
||||||
Loading more...
|
role="alert"
|
||||||
</span>
|
>
|
||||||
|
<LoadingSpinner size={48} />
|
||||||
|
<span class="loading-footer-info">
|
||||||
|
Loading more...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="button-wrapper {showLoadButton ? 'shown' : ''}"
|
||||||
|
aria-hidden={!showLoadButton}
|
||||||
|
>
|
||||||
|
<button type="button"
|
||||||
|
class="primary"
|
||||||
|
on:click="onClickLoadMore(event)">
|
||||||
|
Load more
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
.loading-footer {
|
.loading-footer {
|
||||||
padding: 20px 0 10px;
|
padding: 20px 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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 {
|
.loading-footer-info {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
font-size: 1.3em;
|
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>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
import LoadingSpinner from '../LoadingSpinner.html'
|
import LoadingSpinner from '../LoadingSpinner.html'
|
||||||
import { store } from '../../_store/store'
|
import { store } from '../../_store/store'
|
||||||
|
import { fetchMoreItemsAtBottomOfTimeline } from '../../_actions/timeline'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
store: () => store,
|
store: () => store,
|
||||||
computed: {
|
computed: {
|
||||||
shown: ({ $timelineInitialized, $runningUpdate }) => ($timelineInitialized && $runningUpdate)
|
shown: ({ $timelineInitialized, $runningUpdate, $disableInfiniteScroll }) => (
|
||||||
|
$timelineInitialized && ($disableInfiniteScroll || $runningUpdate)
|
||||||
|
),
|
||||||
|
showLoading: ({ $runningUpdate }) => $runningUpdate,
|
||||||
|
showLoadButton: ({ $runningUpdate, $disableInfiniteScroll }) => !$runningUpdate && $disableInfiniteScroll
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onClickLoadMore (e) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
let { currentInstance, currentTimeline } = this.store.get()
|
||||||
|
/* no await */ fetchMoreItemsAtBottomOfTimeline(currentInstance, currentTimeline)
|
||||||
|
// focus the last item in the timeline; it makes the most sense to me since the button disappears
|
||||||
|
try {
|
||||||
|
// TODO: should probably expose this as an API on the virtual list instead of reaching into the DOM
|
||||||
|
let virtualListItems = document.querySelector('.virtual-list').children
|
||||||
|
let lastItem = virtualListItems[virtualListItems.length - 2] // -2 because the footer is last
|
||||||
|
lastItem.querySelector('article').focus()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
LoadingSpinner
|
LoadingSpinner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
} from '../../_utils/asyncModules'
|
} from '../../_utils/asyncModules'
|
||||||
import { timelines } from '../../_static/timelines'
|
import { timelines } from '../../_static/timelines'
|
||||||
import {
|
import {
|
||||||
fetchTimelineItemsOnScrollToBottom,
|
fetchMoreItemsAtBottomOfTimeline,
|
||||||
setupTimeline,
|
setupTimeline,
|
||||||
showMoreItemsForTimeline,
|
showMoreItemsForTimeline,
|
||||||
showMoreItemsForThread,
|
showMoreItemsForThread,
|
||||||
|
@ -127,13 +127,11 @@
|
||||||
timelineValue !== $firstTimelineItemId &&
|
timelineValue !== $firstTimelineItemId &&
|
||||||
timelineValue
|
timelineValue
|
||||||
),
|
),
|
||||||
itemIds: ({ $timelineItemSummaries }) => (
|
itemIds: ({ $filteredTimelineItemSummaries }) => (
|
||||||
// TODO: filter
|
$filteredTimelineItemSummaries && $filteredTimelineItemSummaries.map(_ => _.id)
|
||||||
$timelineItemSummaries && $timelineItemSummaries.map(_ => _.id)
|
|
||||||
),
|
),
|
||||||
itemIdsToAdd: ({ $timelineItemSummariesToAdd }) => (
|
itemIdsToAdd: ({ $filteredTimelineItemSummariesToAdd }) => (
|
||||||
// TODO: filter
|
$filteredTimelineItemSummariesToAdd && $filteredTimelineItemSummariesToAdd.map(_ => _.id)
|
||||||
$timelineItemSummariesToAdd && $timelineItemSummariesToAdd.map(_ => _.id)
|
|
||||||
),
|
),
|
||||||
headerProps: ({ itemIdsToAdd }) => {
|
headerProps: ({ itemIdsToAdd }) => {
|
||||||
return {
|
return {
|
||||||
|
@ -167,18 +165,16 @@
|
||||||
},
|
},
|
||||||
onScrollToBottom () {
|
onScrollToBottom () {
|
||||||
let { timelineType } = this.get()
|
let { timelineType } = this.get()
|
||||||
let { timelineInitialized, runningUpdate } = this.store.get()
|
let { timelineInitialized, runningUpdate, disableInfiniteScroll } = this.store.get()
|
||||||
if (!timelineInitialized ||
|
if (!timelineInitialized ||
|
||||||
runningUpdate ||
|
runningUpdate ||
|
||||||
|
disableInfiniteScroll ||
|
||||||
timelineType === 'status') { // for status contexts, we've already fetched the whole thread
|
timelineType === 'status') { // for status contexts, we've already fetched the whole thread
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let { currentInstance } = this.store.get()
|
let { currentInstance } = this.store.get()
|
||||||
let { timeline } = this.get()
|
let { timeline } = this.get()
|
||||||
fetchTimelineItemsOnScrollToBottom(
|
/* no await */ fetchMoreItemsAtBottomOfTimeline(currentInstance, timeline)
|
||||||
currentInstance,
|
|
||||||
timeline
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
onScrollToTop () {
|
onScrollToTop () {
|
||||||
let { shouldShowHeader } = this.store.get()
|
let { shouldShowHeader } = this.store.get()
|
||||||
|
@ -190,7 +186,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setupStreaming () {
|
setupStreaming () {
|
||||||
let { currentInstance } = this.store.get()
|
let { currentInstance, disableInfiniteScroll } = this.store.get()
|
||||||
let { timeline, timelineType } = this.get()
|
let { timeline, timelineType } = this.get()
|
||||||
let handleItemIdsToAdd = () => {
|
let handleItemIdsToAdd = () => {
|
||||||
let { itemIdsToAdd } = this.get()
|
let { itemIdsToAdd } = this.get()
|
||||||
|
@ -206,13 +202,17 @@
|
||||||
if (timelineType === 'status') {
|
if (timelineType === 'status') {
|
||||||
// this is a thread, just insert the statuses already
|
// this is a thread, just insert the statuses already
|
||||||
showMoreItemsForThread(currentInstance, timeline)
|
showMoreItemsForThread(currentInstance, timeline)
|
||||||
} else if (scrollTop === 0 && !shouldShowHeader && !showHeader) {
|
} else if (!disableInfiniteScroll && scrollTop === 0 && !shouldShowHeader && !showHeader) {
|
||||||
// if the user is scrolled to the top and we're not showing the header, then
|
// 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"
|
// just insert the statuses. this is "chat room mode"
|
||||||
showMoreItemsForTimeline(currentInstance, timeline)
|
showMoreItemsForTimeline(currentInstance, timeline)
|
||||||
} else {
|
} else {
|
||||||
// user hasn't scrolled to the top, show a header instead
|
// user hasn't scrolled to the top, show a header instead
|
||||||
this.store.setForTimeline(currentInstance, timeline, { shouldShowHeader: true })
|
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')
|
stop('handleItemIdsToAdd')
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,20 @@
|
||||||
import { format } from '../_thirdparty/timeago/timeago'
|
import { format } from '../_thirdparty/timeago/timeago'
|
||||||
import { mark, stop } from '../_utils/marks'
|
import { mark, stop } from '../_utils/marks'
|
||||||
|
|
||||||
export function formatTimeagoDate (date) {
|
// Format a date in the past
|
||||||
|
export function formatTimeagoDate (date, now) {
|
||||||
mark('formatTimeagoDate')
|
mark('formatTimeagoDate')
|
||||||
let res = format(date)
|
// use Math.max() to avoid things like "in 10 seconds" when the timestamps are slightly off
|
||||||
|
let res = format(date, Math.max(now, date))
|
||||||
stop('formatTimeagoDate')
|
stop('formatTimeagoDate')
|
||||||
return res
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -3,15 +3,15 @@
|
||||||
|
|
||||||
<h2>Media</h2>
|
<h2>Media</h2>
|
||||||
<form class="ui-settings">
|
<form class="ui-settings">
|
||||||
<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">Always mark media as sensitive</label>
|
|
||||||
</div>
|
|
||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<input type="checkbox" id="choice-never-mark-media-sensitive"
|
<input type="checkbox" id="choice-never-mark-media-sensitive"
|
||||||
bind:checked="$neverMarkMediaAsSensitive" on:change="onChange(event)">
|
bind:checked="$neverMarkMediaAsSensitive" on:change="onChange(event)">
|
||||||
<label for="choice-never-mark-media-sensitive">Never mark media as sensitive</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<input type="checkbox" id="choice-large-inline-media"
|
<input type="checkbox" id="choice-large-inline-media"
|
||||||
|
@ -32,6 +32,19 @@
|
||||||
bind:checked="$disableCustomScrollbars" on:change="onChange(event)">
|
bind:checked="$disableCustomScrollbars" on:change="onChange(event)">
|
||||||
<label for="choice-disable-custom-scrollbars">Disable custom scrollbars</label>
|
<label for="choice-disable-custom-scrollbars">Disable custom scrollbars</label>
|
||||||
</div>
|
</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">
|
<div class="setting-group">
|
||||||
<input type="checkbox" id="choice-hide-cards"
|
<input type="checkbox" id="choice-hide-cards"
|
||||||
bind:checked="$hideCards" on:change="onChange(event)">
|
bind:checked="$hideCards" on:change="onChange(event)">
|
||||||
|
@ -89,11 +102,13 @@
|
||||||
import SettingsLayout from '../../_components/settings/SettingsLayout.html'
|
import SettingsLayout from '../../_components/settings/SettingsLayout.html'
|
||||||
import ThemeSettings from '../../_components/settings/instance/ThemeSettings.html'
|
import ThemeSettings from '../../_components/settings/instance/ThemeSettings.html'
|
||||||
import { store } from '../../_store/store'
|
import { store } from '../../_store/store'
|
||||||
|
import Tooltip from '../../_components/Tooltip.html'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
SettingsLayout,
|
SettingsLayout,
|
||||||
ThemeSettings
|
ThemeSettings,
|
||||||
|
Tooltip
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onChange (event) {
|
onChange (event) {
|
||||||
|
|
|
@ -8,6 +8,9 @@
|
||||||
<SettingsListRow>
|
<SettingsListRow>
|
||||||
<SettingsListButton href="/settings/instances" label="Instances"/>
|
<SettingsListButton href="/settings/instances" label="Instances"/>
|
||||||
</SettingsListRow>
|
</SettingsListRow>
|
||||||
|
<SettingsListRow>
|
||||||
|
<SettingsListButton href="/settings/wellness" label="Wellness"/>
|
||||||
|
</SettingsListRow>
|
||||||
<SettingsListRow>
|
<SettingsListRow>
|
||||||
<SettingsListButton href="/settings/hotkeys" label="Hotkeys"/>
|
<SettingsListButton href="/settings/hotkeys" label="Hotkeys"/>
|
||||||
</SettingsListRow>
|
</SettingsListRow>
|
||||||
|
|
|
@ -4,9 +4,13 @@
|
||||||
{#if verifyCredentials}
|
{#if verifyCredentials}
|
||||||
<h2>Logged in as:</h2>
|
<h2>Logged in as:</h2>
|
||||||
<InstanceUserProfile {verifyCredentials} />
|
<InstanceUserProfile {verifyCredentials} />
|
||||||
<h2>Push notifications:</h2>
|
<h2>Home timeline filters</h2>
|
||||||
|
<HomeTimelineFilterSettings {instanceName} />
|
||||||
|
<h2>Notification filters</h2>
|
||||||
|
<NotificationFilterSettings {instanceName} />
|
||||||
|
<h2>Push notifications</h2>
|
||||||
<PushNotificationSettings {instanceName} />
|
<PushNotificationSettings {instanceName} />
|
||||||
<h2>Theme:</h2>
|
<h2>Theme</h2>
|
||||||
<ThemeSettings {instanceName} />
|
<ThemeSettings {instanceName} />
|
||||||
|
|
||||||
<InstanceActions {instanceName} />
|
<InstanceActions {instanceName} />
|
||||||
|
@ -23,6 +27,8 @@
|
||||||
import { store } from '../../../_store/store'
|
import { store } from '../../../_store/store'
|
||||||
import SettingsLayout from '../../../_components/settings/SettingsLayout.html'
|
import SettingsLayout from '../../../_components/settings/SettingsLayout.html'
|
||||||
import InstanceUserProfile from '../../../_components/settings/instance/InstanceUserProfile.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 PushNotificationSettings from '../../../_components/settings/instance/PushNotificationSettings.html'
|
||||||
import ThemeSettings from '../../../_components/settings/instance/ThemeSettings.html'
|
import ThemeSettings from '../../../_components/settings/instance/ThemeSettings.html'
|
||||||
import InstanceActions from '../../../_components/settings/instance/InstanceActions.html'
|
import InstanceActions from '../../../_components/settings/instance/InstanceActions.html'
|
||||||
|
@ -43,7 +49,9 @@
|
||||||
InstanceUserProfile,
|
InstanceUserProfile,
|
||||||
PushNotificationSettings,
|
PushNotificationSettings,
|
||||||
ThemeSettings,
|
ThemeSettings,
|
||||||
InstanceActions
|
InstanceActions,
|
||||||
|
HomeTimelineFilterSettings,
|
||||||
|
NotificationFilterSettings
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
<SettingsLayout page='settings/general' label="General">
|
||||||
|
<h1>Wellness Settings</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Wellness settings are designed to reduce the addictive or anxiety-inducing aspects of social media.
|
||||||
|
Choose any options that work well for you.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form class="ui-settings">
|
||||||
|
<div class="setting-group">
|
||||||
|
<input type="checkbox" id="choice-check-all"
|
||||||
|
on:change="onCheckAllChange(event)">
|
||||||
|
<label for="choice-check-all">Enable all</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>Metrics</h2>
|
||||||
|
|
||||||
|
<form class="ui-settings">
|
||||||
|
<div class="setting-group">
|
||||||
|
<input type="checkbox" id="choice-disable-follower-counts"
|
||||||
|
bind:checked="$disableFollowerCounts" on:change="onChange(event)">
|
||||||
|
<label for="choice-disable-follower-counts">
|
||||||
|
Hide follower counts (capped at 10)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting-group">
|
||||||
|
<input type="checkbox" id="choice-disable-reblog-counts"
|
||||||
|
bind:checked="$disableReblogCounts" on:change="onChange(event)">
|
||||||
|
<label for="choice-disable-reblog-counts">Hide boost counts</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting-group">
|
||||||
|
<input type="checkbox" id="choice-disable-fav-counts"
|
||||||
|
bind:checked="$disableFavCounts" on:change="onChange(event)">
|
||||||
|
<label for="choice-disable-fav-counts">Hide favorite counts</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>Notifications</h2>
|
||||||
|
|
||||||
|
<form class="ui-settings">
|
||||||
|
<div class="setting-group">
|
||||||
|
<input type="checkbox" id="choice-disable-unread-notification-counts"
|
||||||
|
bind:checked="$disableNotificationBadge" on:change="onChange(event)">
|
||||||
|
<label for="choice-disable-unread-notification-counts">
|
||||||
|
Hide unread notifications count (i.e. the red dot)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<aside>
|
||||||
|
<SvgIcon href="#fa-info-circle" className="aside-icon" />
|
||||||
|
<span>
|
||||||
|
You can filter or disable notifications in the
|
||||||
|
<a rel="prefetch" href="/settings/instances{$currentInstance ? '/' + $currentInstance : ''}">instance settings</a>.
|
||||||
|
</span>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<h2>UI</h2>
|
||||||
|
|
||||||
|
<form class="ui-settings">
|
||||||
|
<div class="setting-group">
|
||||||
|
<input type="checkbox" id="choice-grayscale"
|
||||||
|
bind:checked="$enableGrayscale" on:change="onChange(event)">
|
||||||
|
<label for="choice-grayscale">Grayscale mode</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p>
|
||||||
|
These settings are partly based on guidelines from the
|
||||||
|
<ExternalLink href="https://humanetech.com">Center for Humane Technology</ExternalLink>.
|
||||||
|
</p>
|
||||||
|
</SettingsLayout>
|
||||||
|
<style>
|
||||||
|
.ui-settings {
|
||||||
|
background: var(--form-bg);
|
||||||
|
border: 1px solid var(--main-border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 20px;
|
||||||
|
line-height: 2em;
|
||||||
|
}
|
||||||
|
.setting-group {
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
aside {
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin: 20px 10px 0px 10px;
|
||||||
|
color: var(--deemphasized-text-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
aside a {
|
||||||
|
text-decoration: underline;
|
||||||
|
color: var(--deemphasized-text-color);
|
||||||
|
}
|
||||||
|
aside span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
:global(.aside-icon) {
|
||||||
|
fill: var(--deemphasized-text-color);
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin: 0 10px 0 5px;
|
||||||
|
min-width: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
import SettingsLayout from '../../_components/settings/SettingsLayout.html'
|
||||||
|
import { store } from '../../_store/store'
|
||||||
|
import ExternalLink from '../../_components/ExternalLink.html'
|
||||||
|
import SvgIcon from '../../_components/SvgIcon.html'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
oncreate () {
|
||||||
|
this.flushChangesToCheckAll()
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
SettingsLayout,
|
||||||
|
ExternalLink,
|
||||||
|
SvgIcon
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
flushChangesToCheckAll () {
|
||||||
|
const {
|
||||||
|
disableFollowerCounts,
|
||||||
|
disableReblogCounts,
|
||||||
|
disableFavCounts,
|
||||||
|
disableNotificationBadge,
|
||||||
|
enableGrayscale
|
||||||
|
} = this.store.get()
|
||||||
|
document.querySelector('#choice-check-all').checked = disableFollowerCounts &&
|
||||||
|
disableReblogCounts &&
|
||||||
|
disableFavCounts &&
|
||||||
|
disableNotificationBadge &&
|
||||||
|
enableGrayscale
|
||||||
|
},
|
||||||
|
onCheckAllChange (e) {
|
||||||
|
let { checked } = e.target
|
||||||
|
this.store.set({
|
||||||
|
disableFollowerCounts: checked,
|
||||||
|
disableReblogCounts: checked,
|
||||||
|
disableFavCounts: checked,
|
||||||
|
disableNotificationBadge: checked,
|
||||||
|
enableGrayscale: checked
|
||||||
|
})
|
||||||
|
this.store.save()
|
||||||
|
},
|
||||||
|
onChange () {
|
||||||
|
this.flushChangesToCheckAll()
|
||||||
|
this.store.save()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
store: () => store
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,8 @@
|
||||||
|
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'
|
|
@ -0,0 +1,32 @@
|
||||||
|
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
|
|
@ -41,6 +41,12 @@ const themes = [
|
||||||
dark: false,
|
dark: false,
|
||||||
color: '#4ab92f'
|
color: '#4ab92f'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'grayscale',
|
||||||
|
label: 'Grayscale',
|
||||||
|
dark: false,
|
||||||
|
color: '#999999'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'ozark',
|
name: 'ozark',
|
||||||
label: 'Ozark',
|
label: 'Ozark',
|
||||||
|
@ -88,6 +94,12 @@ const themes = [
|
||||||
label: 'Pitch Black',
|
label: 'Pitch Black',
|
||||||
dark: true,
|
dark: true,
|
||||||
color: '#000'
|
color: '#000'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dark-grayscale',
|
||||||
|
label: 'Dark Grayscale',
|
||||||
|
dark: true,
|
||||||
|
color: '#666'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import { Store } from 'svelte/store'
|
import { Store } from 'svelte/store'
|
||||||
import { safeLocalStorage as LS } from '../_utils/safeLocalStorage'
|
import { safeLocalStorage as LS } from '../_utils/safeLocalStorage'
|
||||||
import lifecycle from 'page-lifecycle/dist/lifecycle.mjs'
|
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 {
|
export class LocalStorageStore extends Store {
|
||||||
constructor (state, keysToWatch) {
|
constructor (state, keysToWatch) {
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
import { get } from '../../_utils/lodash-lite'
|
import { get } from '../../_utils/lodash-lite'
|
||||||
import { getFirstIdFromItemSummaries, getLastIdFromItemSummaries } from '../../_utils/getIdFromItemSummaries'
|
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) {
|
function computeForTimeline (store, key, defaultValue) {
|
||||||
store.compute(key,
|
store.compute(key,
|
||||||
|
@ -10,6 +19,31 @@ 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) {
|
export function timelineComputations (store) {
|
||||||
computeForTimeline(store, 'timelineItemSummaries', null)
|
computeForTimeline(store, 'timelineItemSummaries', null)
|
||||||
computeForTimeline(store, 'timelineItemSummariesToAdd', null)
|
computeForTimeline(store, 'timelineItemSummariesToAdd', null)
|
||||||
|
@ -41,16 +75,110 @@ export function timelineComputations (store) {
|
||||||
getLastIdFromItemSummaries(timelineItemSummaries)
|
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',
|
store.compute('numberOfNotifications',
|
||||||
[`timelineData_timelineItemSummariesToAdd`, 'currentInstance'],
|
['filteredTimelineNotificationItemSummaries', 'disableNotificationBadge'],
|
||||||
(root, currentInstance) => (
|
(filteredTimelineNotificationItemSummaries, disableNotificationBadge) => (
|
||||||
(root && root[currentInstance] && root[currentInstance].notifications &&
|
(!disableNotificationBadge && filteredTimelineNotificationItemSummaries)
|
||||||
root[currentInstance].notifications.length) || 0
|
? filteredTimelineNotificationItemSummaries.length
|
||||||
|
: 0
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
store.compute('hasNotifications',
|
store.compute('hasNotifications',
|
||||||
['numberOfNotifications', 'currentPage'],
|
['numberOfNotifications', 'currentPage'],
|
||||||
(numberOfNotifications, currentPage) => currentPage !== 'notifications' && !!numberOfNotifications
|
(numberOfNotifications, currentPage) => (
|
||||||
|
currentPage !== 'notifications' && !!numberOfNotifications
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { get } from '../../_utils/lodash-lite'
|
||||||
|
|
||||||
export function instanceMixins (Store) {
|
export function instanceMixins (Store) {
|
||||||
Store.prototype.setComposeData = function (realm, obj) {
|
Store.prototype.setComposeData = function (realm, obj) {
|
||||||
let { composeData, currentInstance } = this.get()
|
let { composeData, currentInstance } = this.get()
|
||||||
|
@ -20,4 +22,18 @@ export function instanceMixins (Store) {
|
||||||
}
|
}
|
||||||
this.set({ composeData })
|
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 })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { switchToTheme } from '../../_utils/themeEngine'
|
||||||
|
|
||||||
|
const style = process.browser && document.getElementById('theGrayscaleStyle')
|
||||||
|
|
||||||
|
export function grayscaleObservers (store) {
|
||||||
|
if (!process.browser) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
store.observe('enableGrayscale', enableGrayscale => {
|
||||||
|
const { instanceThemes, currentInstance } = store.get()
|
||||||
|
const theme = instanceThemes && instanceThemes[currentInstance]
|
||||||
|
style.setAttribute('media', enableGrayscale ? 'all' : 'only x') // disable or enable the style
|
||||||
|
switchToTheme(theme, enableGrayscale)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,21 @@
|
||||||
import { onlineObservers } from './onlineObservers'
|
import { onlineObservers } from './onlineObservers'
|
||||||
|
import { nowObservers } from './nowObservers'
|
||||||
import { navObservers } from './navObservers'
|
import { navObservers } from './navObservers'
|
||||||
import { pageVisibilityObservers } from './pageVisibilityObservers'
|
import { pageVisibilityObservers } from './pageVisibilityObservers'
|
||||||
import { resizeObservers } from './resizeObservers'
|
import { resizeObservers } from './resizeObservers'
|
||||||
import { setupLoggedInObservers } from './setupLoggedInObservers'
|
import { setupLoggedInObservers } from './setupLoggedInObservers'
|
||||||
import { logOutObservers } from './logOutObservers'
|
import { logOutObservers } from './logOutObservers'
|
||||||
import { touchObservers } from './touchObservers'
|
import { touchObservers } from './touchObservers'
|
||||||
|
import { grayscaleObservers } from './grayscaleObservers'
|
||||||
|
|
||||||
export function observers (store) {
|
export function observers (store) {
|
||||||
onlineObservers(store)
|
onlineObservers(store)
|
||||||
|
nowObservers(store)
|
||||||
navObservers(store)
|
navObservers(store)
|
||||||
pageVisibilityObservers(store)
|
pageVisibilityObservers(store)
|
||||||
resizeObservers(store)
|
resizeObservers(store)
|
||||||
touchObservers(store)
|
touchObservers(store)
|
||||||
logOutObservers(store)
|
logOutObservers(store)
|
||||||
|
grayscaleObservers(store)
|
||||||
setupLoggedInObservers(store)
|
setupLoggedInObservers(store)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,6 @@ const NOTIFY_OFFLINE_LIMIT = 1
|
||||||
|
|
||||||
let notifyCount = 0
|
let notifyCount = 0
|
||||||
|
|
||||||
let offlineStyle = process.browser && document.getElementById('theOfflineStyle')
|
|
||||||
|
|
||||||
// debounce to avoid notifying for a short connection issue
|
// debounce to avoid notifying for a short connection issue
|
||||||
const notifyOffline = debounce(() => {
|
const notifyOffline = debounce(() => {
|
||||||
if (process.browser && !navigator.onLine && ++notifyCount <= NOTIFY_OFFLINE_LIMIT) {
|
if (process.browser && !navigator.onLine && ++notifyCount <= NOTIFY_OFFLINE_LIMIT) {
|
||||||
|
@ -19,20 +17,9 @@ export function onlineObservers (store) {
|
||||||
if (!process.browser) {
|
if (!process.browser) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let meta = document.getElementById('theThemeColor')
|
|
||||||
let oldTheme = meta.content
|
|
||||||
|
|
||||||
store.observe('online', online => {
|
store.observe('online', online => {
|
||||||
// "only x" ensures the <style> tag does not have any effect
|
if (!online) {
|
||||||
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()
|
notifyOffline()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export function safeParse (str) {
|
||||||
|
return !str ? undefined : (str === 'undefined' ? undefined : JSON.parse(str))
|
||||||
|
}
|
|
@ -12,13 +12,20 @@ const persistedState = {
|
||||||
currentRegisteredInstance: undefined,
|
currentRegisteredInstance: undefined,
|
||||||
// we disable scrollbars by default on iOS
|
// we disable scrollbars by default on iOS
|
||||||
disableCustomScrollbars: process.browser && /iP(?:hone|ad|od)/.test(navigator.userAgent),
|
disableCustomScrollbars: process.browser && /iP(?:hone|ad|od)/.test(navigator.userAgent),
|
||||||
|
disableFavCounts: false,
|
||||||
|
disableFollowerCounts: false,
|
||||||
disableHotkeys: false,
|
disableHotkeys: false,
|
||||||
|
disableInfiniteScroll: false,
|
||||||
disableLongAriaLabels: false,
|
disableLongAriaLabels: false,
|
||||||
|
disableNotificationBadge: false,
|
||||||
|
disableReblogCounts: false,
|
||||||
disableTapOnStatus: false,
|
disableTapOnStatus: false,
|
||||||
|
enableGrayscale: false,
|
||||||
hideCards: false,
|
hideCards: false,
|
||||||
largeInlineMedia: false,
|
largeInlineMedia: false,
|
||||||
instanceNameInSearch: '',
|
instanceNameInSearch: '',
|
||||||
instanceThemes: {},
|
instanceThemes: {},
|
||||||
|
instanceSettings: {},
|
||||||
loggedInInstances: {},
|
loggedInInstances: {},
|
||||||
loggedInInstancesInOrder: [],
|
loggedInInstancesInOrder: [],
|
||||||
markMediaAsSensitive: false,
|
markMediaAsSensitive: false,
|
||||||
|
@ -38,6 +45,7 @@ const nonPersistedState = {
|
||||||
instanceLists: {},
|
instanceLists: {},
|
||||||
online: !process.browser || navigator.onLine,
|
online: !process.browser || navigator.onLine,
|
||||||
pinnedStatuses: {},
|
pinnedStatuses: {},
|
||||||
|
polls: {},
|
||||||
pushNotificationsSupport:
|
pushNotificationsSupport:
|
||||||
process.browser &&
|
process.browser &&
|
||||||
('serviceWorker' in navigator &&
|
('serviceWorker' in navigator &&
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
// "lite" version of the store used in the inline script. Purely read-only,
|
||||||
|
// does not implement non-LocalStorage store features.
|
||||||
|
|
||||||
|
import { safeParse } from './safeParse'
|
||||||
|
import { testHasLocalStorageOnce } from '../_utils/testStorage'
|
||||||
|
|
||||||
|
const hasLocalStorage = testHasLocalStorageOnce()
|
||||||
|
|
||||||
|
export const storeLite = {
|
||||||
|
get () {
|
||||||
|
return new Proxy({}, {
|
||||||
|
get: function (obj, prop) {
|
||||||
|
if (!(prop in obj)) {
|
||||||
|
obj[prop] = hasLocalStorage && safeParse(localStorage.getItem(`store_${prop}`))
|
||||||
|
}
|
||||||
|
return obj[prop]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,7 +4,7 @@
|
||||||
* Contract: i@hust.cc
|
* Contract: i@hust.cc
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var IndexMapEn = 'second_minute_hour_day_week_month_year'.split('_')
|
var IndexMapEn = ['second', 'minute', 'hour', 'day', 'week', 'month', 'year']
|
||||||
var SEC_ARRAY = [60, 60, 24, 7, 365 / 7 / 12, 12]
|
var SEC_ARRAY = [60, 60, 24, 7, 365 / 7 / 12, 12]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,16 +63,14 @@ function formatDiff (diff) {
|
||||||
* @param nowDate
|
* @param nowDate
|
||||||
* @returns {number}
|
* @returns {number}
|
||||||
*/
|
*/
|
||||||
function diffSec (date) {
|
function diffSec (date, now) {
|
||||||
var nowDate = new Date()
|
return (now - date) / 1000
|
||||||
var otherDate = new Date(date)
|
|
||||||
return (nowDate - otherDate) / 1000
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by hustcc on 18/5/20.
|
* Created by hustcc on 18/5/20.
|
||||||
* Contract: i@hust.cc
|
* Contract: i@hust.cc
|
||||||
*/
|
*/
|
||||||
export function format (date) {
|
export function format (date, now) {
|
||||||
return formatDiff(diffSec(date))
|
return formatDiff(diffSec(date, now))
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014, 2016-2017, Jon Schlinkert
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
|
@ -0,0 +1,63 @@
|
||||||
|
// via https://github.com/jonschlinkert/unescape/blob/98d1e52/index.js
|
||||||
|
|
||||||
|
const chars = {
|
||||||
|
'"': '"',
|
||||||
|
'"': '"',
|
||||||
|
|
||||||
|
''': '\'',
|
||||||
|
''': '\'',
|
||||||
|
|
||||||
|
'&': '&',
|
||||||
|
'&': '&',
|
||||||
|
|
||||||
|
'>': '>',
|
||||||
|
'>': '>',
|
||||||
|
|
||||||
|
'<': '<',
|
||||||
|
'<': '<',
|
||||||
|
|
||||||
|
'¢': '¢',
|
||||||
|
'¢': '¢',
|
||||||
|
|
||||||
|
'©': '©',
|
||||||
|
'©': '©',
|
||||||
|
|
||||||
|
'€': '€',
|
||||||
|
'€': '€',
|
||||||
|
|
||||||
|
'£': '£',
|
||||||
|
'£': '£',
|
||||||
|
|
||||||
|
'®': '®',
|
||||||
|
'®': '®',
|
||||||
|
|
||||||
|
'¥': '¥',
|
||||||
|
'¥': '¥',
|
||||||
|
|
||||||
|
' ': ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
let regex
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert HTML entities to HTML characters.
|
||||||
|
*
|
||||||
|
* @param {String} `str` String with HTML entities to un-escape.
|
||||||
|
* @return {String}
|
||||||
|
*/
|
||||||
|
|
||||||
|
function unescape (str) {
|
||||||
|
regex = regex || toRegex(chars)
|
||||||
|
return str.replace(regex, m => chars[m])
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRegex (chars) {
|
||||||
|
var keys = Object.keys(chars).join('|')
|
||||||
|
return new RegExp('(' + keys + ')', 'g')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expose `unescape`
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { unescape }
|
|
@ -0,0 +1,17 @@
|
||||||
|
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)
|
||||||
|
}
|
|
@ -43,3 +43,7 @@ export const importToast = () => import(
|
||||||
export const importSnackbar = () => import(
|
export const importSnackbar = () => import(
|
||||||
/* webpackChunkName: 'Snackbar.html' */ '../_components/snackbar/Snackbar.html'
|
/* webpackChunkName: 'Snackbar.html' */ '../_components/snackbar/Snackbar.html'
|
||||||
).then(getDefault)
|
).then(getDefault)
|
||||||
|
|
||||||
|
export const importComposeBox = () => import(
|
||||||
|
/* webpackChunkName: 'ComposeBox.html' */ '../_components/compose/ComposeBox.html'
|
||||||
|
).then(getDefault)
|
||||||
|
|
|
@ -1,11 +1,22 @@
|
||||||
import { snackbar } from '../_components/snackbar/snackbar'
|
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) {
|
function onUpdateFound (registration) {
|
||||||
const newWorker = registration.installing
|
const newWorker = registration.installing
|
||||||
|
|
||||||
newWorker.addEventListener('statechange', async () => {
|
newWorker.addEventListener('statechange', async () => {
|
||||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
||||||
snackbar.announce('App update available.', 'Reload', () => document.location.reload(true))
|
snackbar.announce('App update available.', 'Reload', async () => {
|
||||||
|
await skipWaiting()
|
||||||
|
document.location.reload(true)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
let meta = process.browser && document.getElementById('theThemeColor')
|
const prefersDarkTheme = process.browser && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
let offlineStyle = process.browser && document.getElementById('theOfflineStyle')
|
const meta = process.browser && document.getElementById('theThemeColor')
|
||||||
let prefersDarkTheme = process.browser && window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
||||||
|
|
||||||
export const DEFAULT_LIGHT_THEME = 'default'
|
export const INLINE_THEME = 'default' // theme that does not require external CSS
|
||||||
export const DEFAULT_DARK_THEME = 'ozark'
|
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_THEME = prefersDarkTheme ? DEFAULT_DARK_THEME : DEFAULT_LIGHT_THEME
|
export const DEFAULT_THEME = prefersDarkTheme ? DEFAULT_DARK_THEME : DEFAULT_LIGHT_THEME
|
||||||
|
|
||||||
function getExistingThemeLink () {
|
function getExistingThemeLink () {
|
||||||
|
@ -31,14 +31,16 @@ function loadCSS (href) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// inserting before the offline <style> ensures that the offline style wins when offline
|
document.head.appendChild(link)
|
||||||
document.head.insertBefore(link, offlineStyle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function switchToTheme (themeName = DEFAULT_THEME) {
|
export function switchToTheme (themeName = DEFAULT_THEME, enableGrayscale) {
|
||||||
|
if (enableGrayscale) {
|
||||||
|
themeName = prefersDarkTheme ? 'grayscale-dark' : 'grayscale'
|
||||||
|
}
|
||||||
let themeColor = window.__themeColors[themeName]
|
let themeColor = window.__themeColors[themeName]
|
||||||
meta.content = themeColor || window.__themeColors[DEFAULT_THEME]
|
meta.content = themeColor || window.__themeColors[DEFAULT_THEME]
|
||||||
if (themeName !== DEFAULT_LIGHT_THEME) {
|
if (themeName !== INLINE_THEME) {
|
||||||
loadCSS(`/theme-${themeName}.css`)
|
loadCSS(`/theme-${themeName}.css`)
|
||||||
} else {
|
} else {
|
||||||
resetExistingTheme()
|
resetExistingTheme()
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<Title name="Wellness Settings" settingsPage={true} />
|
||||||
|
|
||||||
|
<LazyPage {pageComponent} {params} />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Title from '../_components/Title.html'
|
||||||
|
import LazyPage from '../_components/LazyPage.html'
|
||||||
|
import pageComponent from '../_pages/settings/wellness.html'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
|
||||||
|
Title,
|
||||||
|
LazyPage
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
pageComponent
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -87,7 +87,7 @@
|
||||||
--status-direct-background: #{darken($body-bg-color, 5%)};
|
--status-direct-background: #{darken($body-bg-color, 5%)};
|
||||||
--main-theme-color: #{$main-theme-color};
|
--main-theme-color: #{$main-theme-color};
|
||||||
--warning-color: #{#e01f19};
|
--warning-color: #{#e01f19};
|
||||||
--alt-input-bg: #{rgba($main-bg-color, 0.7)};
|
--alt-input-bg: #{rgba($main-bg-color, 0.9)};
|
||||||
|
|
||||||
--muted-modal-text: #{$secondary-text-color};
|
--muted-modal-text: #{$secondary-text-color};
|
||||||
--muted-modal-bg: #{transparent};
|
--muted-modal-bg: #{transparent};
|
||||||
|
@ -112,4 +112,10 @@
|
||||||
|
|
||||||
--tooltip-bg: rgba(30, 30, 30, 0.9);
|
--tooltip-bg: rgba(30, 30, 30, 0.9);
|
||||||
--tooltip-color: white;
|
--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};
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
--status-direct-background: #{darken($body-bg-color, 5%)};
|
--status-direct-background: #{darken($body-bg-color, 5%)};
|
||||||
--main-theme-color: #{$main-theme-color};
|
--main-theme-color: #{$main-theme-color};
|
||||||
--warning-color: #{#c7423d};
|
--warning-color: #{#c7423d};
|
||||||
--alt-input-bg: #{rgba($main-bg-color, 0.7)};
|
--alt-input-bg: #{rgba($main-bg-color, 0.9)};
|
||||||
|
|
||||||
--muted-modal-bg: #{transparent};
|
--muted-modal-bg: #{transparent};
|
||||||
--muted-modal-focus: #{#999};
|
--muted-modal-focus: #{#999};
|
||||||
|
@ -46,4 +46,6 @@
|
||||||
--tab-bg-hover-non-selected: #{darken($main-bg-color, 1%)};
|
--tab-bg-hover-non-selected: #{darken($main-bg-color, 1%)};
|
||||||
|
|
||||||
--toast-anchor-color: #{$anchor-color};
|
--toast-anchor-color: #{$anchor-color};
|
||||||
|
|
||||||
|
--length-indicator-color: var(--action-button-fill-color);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,11 @@ $compose-background: lighten($main-theme-color, 32%);
|
||||||
:root {
|
:root {
|
||||||
--settings-list-item-text: #{$main-text-color};
|
--settings-list-item-text: #{$main-text-color};
|
||||||
--settings-list-item-text-hover: #{$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: #{$anchor-color};
|
||||||
--action-button-fill-color-pressed-hover: #{darken($anchor-color, 2%)};
|
--action-button-fill-color-pressed-hover: #{darken($anchor-color, 2%)};
|
||||||
--action-button-fill-color-pressed-active: #{darken($anchor-color, 15%)};
|
--action-button-fill-color-pressed-active: #{darken($anchor-color, 15%)};
|
||||||
|
@ -30,4 +34,4 @@ $compose-background: lighten($main-theme-color, 32%);
|
||||||
--nav-text-color-hover: #{$main-text-color};
|
--nav-text-color-hover: #{$main-text-color};
|
||||||
--nav-a-selected-border: #{$anchor-color};
|
--nav-a-selected-border: #{$anchor-color};
|
||||||
--nav-a-selected-border-hover: #{$anchor-color};
|
--nav-a-selected-border-hover: #{$anchor-color};
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
$main-theme-color: #444;
|
||||||
|
$main-bg-color: #202020;
|
||||||
|
$body-bg-color: darken($main-bg-color, 5%);
|
||||||
|
$anchor-color: #999;
|
||||||
|
$main-text-color: #FFF;
|
||||||
|
$border-color: lighten($body-bg-color, 10%);
|
||||||
|
$secondary-text-color: white;
|
||||||
|
$toast-border: #fafafa;
|
||||||
|
$toast-bg: #333;
|
||||||
|
$focus-outline: lighten($main-theme-color, 50%);
|
||||||
|
$compose-background: lighten($main-theme-color, 52%);
|
||||||
|
|
||||||
|
@import "_base.scss";
|
||||||
|
@import "_dark.scss";
|
||||||
|
@import "_dark_navbar.scss";
|
||||||
|
@import "_dark_scrollbars.scss";
|
|
@ -1,6 +1,6 @@
|
||||||
$main-theme-color: #999999;
|
$main-theme-color: #666;
|
||||||
$body-bg-color: lighten($main-theme-color, 38%);
|
$body-bg-color: lighten($main-theme-color, 38%);
|
||||||
$anchor-color: $main-theme-color;
|
$anchor-color: lighten($main-theme-color, 5%);
|
||||||
$main-text-color: #333;
|
$main-text-color: #333;
|
||||||
$border-color: #dadada;
|
$border-color: #dadada;
|
||||||
$main-bg-color: white;
|
$main-bg-color: white;
|
||||||
|
@ -11,4 +11,4 @@ $focus-outline: lighten($main-theme-color, 15%);
|
||||||
$compose-background: lighten($main-theme-color, 17%);
|
$compose-background: lighten($main-theme-color, 17%);
|
||||||
|
|
||||||
@import "_base.scss";
|
@import "_base.scss";
|
||||||
@import "_light_scrollbars.scss";
|
@import "_light_scrollbars.scss";
|
|
@ -29,9 +29,9 @@ $compose-background: darken($main-theme-color, 12%);
|
||||||
--form-border: #{darken($border-color, 10%)};
|
--form-border: #{darken($border-color, 10%)};
|
||||||
|
|
||||||
|
|
||||||
--action-button-fill-color: #{$main-theme-color};
|
--action-button-fill-color: #{lighten($main-theme-color, 5%)};
|
||||||
--action-button-fill-color-hover: #{lighten($main-theme-color, 4%)};
|
--action-button-fill-color-hover: #{lighten($main-theme-color, 12%)};
|
||||||
--action-button-fill-color-active: #{darken($main-theme-color, 13%)};
|
--action-button-fill-color-active: #{darken($main-theme-color, 15%)};
|
||||||
--action-button-fill-color-pressed: #{lighten($main-theme-color, 20%)};
|
--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-hover: #{lighten($main-theme-color, 24%)};
|
||||||
--action-button-fill-color-pressed-active: #{lighten($main-theme-color, 7%)};
|
--action-button-fill-color-pressed-active: #{lighten($main-theme-color, 7%)};
|
||||||
|
|
|
@ -35,6 +35,9 @@ self.addEventListener('install', event => {
|
||||||
caches.open(WEBPACK_ASSETS).then(cache => cache.addAll(webpackAssets)),
|
caches.open(WEBPACK_ASSETS).then(cache => cache.addAll(webpackAssets)),
|
||||||
caches.open(ASSETS).then(cache => cache.addAll(assets))
|
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()
|
self.skipWaiting()
|
||||||
})())
|
})())
|
||||||
})
|
})
|
||||||
|
@ -46,7 +49,7 @@ self.addEventListener('activate', event => {
|
||||||
// delete old asset/ondemand caches
|
// delete old asset/ondemand caches
|
||||||
for (let key of keys) {
|
for (let key of keys) {
|
||||||
if (key !== ASSETS &&
|
if (key !== ASSETS &&
|
||||||
!key.startsWith('webpack_assets_')) {
|
!key.startsWith('webpack_assets_')) {
|
||||||
await caches.delete(key)
|
await caches.delete(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,86 +134,78 @@ async function showSimpleNotification (data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showRichNotification (data, notification) {
|
async function showRichNotification (data, notification) {
|
||||||
const { origin } = new URL(data.icon)
|
const { icon, body } = data
|
||||||
|
const tag = notification.id
|
||||||
|
const { origin } = self.location
|
||||||
|
const badge = '/icon-push-badge.png'
|
||||||
|
|
||||||
switch (notification.type) {
|
switch (notification.type) {
|
||||||
case 'follow': {
|
case 'follow': {
|
||||||
await self.registration.showNotification(data.title, {
|
await self.registration.showNotification(data.title, {
|
||||||
icon: data.icon,
|
badge,
|
||||||
body: data.body,
|
icon,
|
||||||
tag: notification.id,
|
body,
|
||||||
|
tag,
|
||||||
data: {
|
data: {
|
||||||
url: `${self.location.origin}/accounts/${notification.account.id}`
|
url: `${origin}/accounts/${notification.account.id}`
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'mention': {
|
case 'reblog':
|
||||||
const actions = [{
|
case 'favourite':
|
||||||
action: 'favourite',
|
case 'poll':
|
||||||
title: 'Favourite'
|
await self.registration.showNotification(data.title, {
|
||||||
}]
|
badge,
|
||||||
|
icon,
|
||||||
if ('reply' in NotificationEvent.prototype) {
|
body,
|
||||||
actions.splice(0, 0, {
|
tag,
|
||||||
action: 'reply',
|
data: {
|
||||||
type: 'text',
|
url: `${origin}/statuses/${notification.status.id}`
|
||||||
title: 'Reply'
|
}
|
||||||
})
|
})
|
||||||
}
|
break
|
||||||
|
case 'mention':
|
||||||
if (['public', 'unlisted'].includes(notification.status.visibility)) {
|
const isPublic = ['public', 'unlisted'].includes(notification.status.visibility)
|
||||||
actions.push({
|
const actions = [
|
||||||
|
isPublic && {
|
||||||
action: 'reblog',
|
action: 'reblog',
|
||||||
|
icon: '/icon-push-fa-retweet.png', // generated manually from font-awesome-svg
|
||||||
title: 'Boost'
|
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, {
|
await self.registration.showNotification(data.title, {
|
||||||
icon: data.icon,
|
badge,
|
||||||
body: data.body,
|
icon,
|
||||||
tag: notification.id,
|
body,
|
||||||
|
tag,
|
||||||
data: {
|
data: {
|
||||||
instance: origin,
|
instance: new URL(data.icon).origin,
|
||||||
status_id: notification.status.id,
|
status_id: notification.status.id,
|
||||||
access_token: data.access_token,
|
access_token: data.access_token,
|
||||||
url: `${self.location.origin}/statuses/${notification.status.id}`
|
url: `${origin}/statuses/${notification.status.id}`
|
||||||
},
|
},
|
||||||
actions
|
actions
|
||||||
})
|
})
|
||||||
break
|
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 cloneNotification = notification => {
|
||||||
const clone = { }
|
const clone = {}
|
||||||
|
|
||||||
// Object.assign() does not work with notifications
|
|
||||||
for (let k in notification) {
|
for (let k in notification) {
|
||||||
clone[k] = notification[k]
|
// 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]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return clone
|
return clone
|
||||||
|
@ -227,21 +222,19 @@ const updateNotificationWithoutAction = (notification, action) => {
|
||||||
self.addEventListener('notificationclick', event => {
|
self.addEventListener('notificationclick', event => {
|
||||||
event.waitUntil((async () => {
|
event.waitUntil((async () => {
|
||||||
switch (event.action) {
|
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': {
|
case 'reblog': {
|
||||||
await post(`${event.notification.data.instance}/api/v1/statuses/${event.notification.data.status_id}/reblog`, null, { 'Authorization': `Bearer ${event.notification.data.access_token}` })
|
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 updateNotificationWithoutAction(event.notification, 'reblog')
|
await updateNotificationWithoutAction(event.notification, 'reblog')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'favourite': {
|
case 'favourite': {
|
||||||
await post(`${event.notification.data.instance}/api/v1/statuses/${event.notification.data.status_id}/favourite`, null, { 'Authorization': `Bearer ${event.notification.data.access_token}` })
|
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 updateNotificationWithoutAction(event.notification, 'favourite')
|
await updateNotificationWithoutAction(event.notification, 'favourite')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -253,3 +246,11 @@ self.addEventListener('notificationclick', event => {
|
||||||
}
|
}
|
||||||
})())
|
})())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
self.addEventListener('message', (event) => {
|
||||||
|
switch (event.data) {
|
||||||
|
case 'skip-waiting':
|
||||||
|
self.skipWaiting()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 505 B |
Binary file not shown.
After Width: | Height: | Size: 905 B |
|
@ -9,6 +9,8 @@ import { followAccount, unfollowAccount } from '../src/routes/_api/follow'
|
||||||
import { updateCredentials } from '../src/routes/_api/updateCredentials'
|
import { updateCredentials } from '../src/routes/_api/updateCredentials'
|
||||||
import { reblogStatus } from '../src/routes/_api/reblog'
|
import { reblogStatus } from '../src/routes/_api/reblog'
|
||||||
import { submitMedia } from './submitMedia'
|
import { submitMedia } from './submitMedia'
|
||||||
|
import { voteOnPoll } from '../src/routes/_api/polls'
|
||||||
|
import { POLL_EXPIRY_DEFAULT } from '../src/routes/_static/polls'
|
||||||
|
|
||||||
global.fetch = fetch
|
global.fetch = fetch
|
||||||
global.File = FileApi.File
|
global.File = FileApi.File
|
||||||
|
@ -68,3 +70,15 @@ export async function unfollowAs (username, userToFollow) {
|
||||||
export async function updateUserDisplayNameAs (username, displayName) {
|
export async function updateUserDisplayNameAs (username, displayName) {
|
||||||
return updateCredentials(instanceName, users[username].accessToken, { display_name: 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()))
|
||||||
|
}
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import {
|
import {
|
||||||
getUrl, notificationFiltersAll, notificationFiltersMention,
|
getUrl, notificationsTabAll, notificationsTabMentions,
|
||||||
notificationsNavButton, validateTimeline
|
notificationsNavButton, validateTimeline
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { loginAsFoobar } from '../roles'
|
import { loginAsFoobar } from '../roles'
|
||||||
import { notificationsMentions, notifications } from '../fixtures'
|
import { notificationsMentions, notifications } from '../fixtures'
|
||||||
|
|
||||||
fixture`033-notification-filters.js`
|
fixture`033-notification-mentions.js`
|
||||||
.page`http://localhost:4002`
|
.page`http://localhost:4002`
|
||||||
|
|
||||||
test('Shows notification filters', async t => {
|
test('Shows notification mentions', async t => {
|
||||||
await loginAsFoobar(t)
|
await loginAsFoobar(t)
|
||||||
await t
|
await t
|
||||||
.click(notificationsNavButton)
|
.click(notificationsNavButton)
|
||||||
.expect(getUrl()).match(/\/notifications$/)
|
.expect(getUrl()).match(/\/notifications$/)
|
||||||
.click(notificationFiltersMention)
|
.click(notificationsTabMentions)
|
||||||
.expect(getUrl()).match(/\/notifications\/mentions$/)
|
.expect(getUrl()).match(/\/notifications\/mentions$/)
|
||||||
await validateTimeline(t, notificationsMentions)
|
await validateTimeline(t, notificationsMentions)
|
||||||
await t.click(notificationFiltersAll)
|
await t.click(notificationsTabAll)
|
||||||
.expect(getUrl()).match(/\/notifications$/)
|
.expect(getUrl()).match(/\/notifications$/)
|
||||||
await validateTimeline(t, notifications)
|
await validateTimeline(t, notifications)
|
||||||
})
|
})
|
|
@ -0,0 +1,23 @@
|
||||||
|
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'
|
||||||
|
}))
|
||||||
|
})
|
|
@ -0,0 +1,48 @@
|
||||||
|
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))
|
||||||
|
})
|
|
@ -0,0 +1,39 @@
|
||||||
|
import {
|
||||||
|
settingsNavButton,
|
||||||
|
homeNavButton,
|
||||||
|
disableInfiniteScroll,
|
||||||
|
scrollToStatus,
|
||||||
|
loadMoreButton, getFirstVisibleStatus, scrollFromStatusToStatus, sleep, getActiveElementAriaPosInSet
|
||||||
|
} from '../utils'
|
||||||
|
import { loginAsFoobar } from '../roles'
|
||||||
|
import { Selector as $ } from 'testcafe'
|
||||||
|
|
||||||
|
fixture`036-disable-infinite-load.js`
|
||||||
|
.page`http://localhost:4002`
|
||||||
|
|
||||||
|
test('Can disable loading items at bottom of timeline', async t => {
|
||||||
|
await loginAsFoobar(t)
|
||||||
|
await t.click(settingsNavButton)
|
||||||
|
.click($('a').withText('General'))
|
||||||
|
.click(disableInfiniteScroll)
|
||||||
|
.expect(disableInfiniteScroll.checked).ok()
|
||||||
|
.click(homeNavButton)
|
||||||
|
.expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('20')
|
||||||
|
await scrollToStatus(t, 20)
|
||||||
|
await t
|
||||||
|
.click(loadMoreButton)
|
||||||
|
.expect(getActiveElementAriaPosInSet()).eql('20')
|
||||||
|
.expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('40')
|
||||||
|
await scrollFromStatusToStatus(t, 20, 40)
|
||||||
|
await t
|
||||||
|
.click(loadMoreButton)
|
||||||
|
.expect(getActiveElementAriaPosInSet()).eql('40')
|
||||||
|
.expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('47')
|
||||||
|
await scrollFromStatusToStatus(t, 40, 47)
|
||||||
|
await t
|
||||||
|
.click(loadMoreButton)
|
||||||
|
await sleep(1000)
|
||||||
|
await t
|
||||||
|
.expect(loadMoreButton.exists).ok()
|
||||||
|
.expect(getFirstVisibleStatus().getAttribute('aria-setsize')).eql('47')
|
||||||
|
})
|
|
@ -1,12 +1,12 @@
|
||||||
import {
|
import {
|
||||||
getNthStatusContent,
|
getNthStatusContent,
|
||||||
getUrl, notificationFiltersAll, notificationFiltersMention,
|
getUrl, notificationsTabAll, notificationsTabMentions,
|
||||||
notificationsNavButton, sleep
|
notificationsNavButton, sleep
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { loginAsFoobar } from '../roles'
|
import { loginAsFoobar } from '../roles'
|
||||||
import { favoriteStatusAs, postAs } from '../serverActions'
|
import { favoriteStatusAs, postAs } from '../serverActions'
|
||||||
|
|
||||||
fixture`123-notification-filters.js`
|
fixture`123-notification-mentions.js`
|
||||||
.page`http://localhost:4002`
|
.page`http://localhost:4002`
|
||||||
|
|
||||||
// maybe in the "mentions" view it should prevent the notification icon from showing (1), (2) etc
|
// 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
|
await t
|
||||||
.click(notificationsNavButton)
|
.click(notificationsNavButton)
|
||||||
.expect(getUrl()).match(/\/notifications$/)
|
.expect(getUrl()).match(/\/notifications$/)
|
||||||
.click(notificationFiltersMention)
|
.click(notificationsTabMentions)
|
||||||
.expect(getUrl()).match(/\/notifications\/mentions$/)
|
.expect(getUrl()).match(/\/notifications\/mentions$/)
|
||||||
await sleep(2000)
|
await sleep(2000)
|
||||||
await postAs('admin', 'hey @foobar I am mentioning you')
|
await postAs('admin', 'hey @foobar I am mentioning you')
|
||||||
|
@ -27,7 +27,7 @@ test('Handles incoming notifications that are mentions', async t => {
|
||||||
timeout
|
timeout
|
||||||
})
|
})
|
||||||
.expect(getNthStatusContent(1).innerText).contains('hey @foobar I am mentioning you')
|
.expect(getNthStatusContent(1).innerText).contains('hey @foobar I am mentioning you')
|
||||||
.click(notificationFiltersAll)
|
.click(notificationsTabAll)
|
||||||
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (current page)', { timeout })
|
.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
|
await t
|
||||||
.click(notificationsNavButton)
|
.click(notificationsNavButton)
|
||||||
.expect(getUrl()).match(/\/notifications$/)
|
.expect(getUrl()).match(/\/notifications$/)
|
||||||
.click(notificationFiltersMention)
|
.click(notificationsTabMentions)
|
||||||
.expect(getUrl()).match(/\/notifications\/mentions$/)
|
.expect(getUrl()).match(/\/notifications\/mentions$/)
|
||||||
await sleep(2000)
|
await sleep(2000)
|
||||||
await postAs('admin', 'woot I am mentioning you again @foobar')
|
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 sleep(2000)
|
||||||
await t
|
await t
|
||||||
.expect(getNthStatusContent(1).innerText).contains('woot I am mentioning you again @foobar')
|
.expect(getNthStatusContent(1).innerText).contains('woot I am mentioning you again @foobar')
|
||||||
.click(notificationFiltersAll)
|
.click(notificationsTabAll)
|
||||||
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (current page)', { timeout })
|
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (current page)', { timeout })
|
||||||
await t
|
await t
|
||||||
.expect(getNthStatusContent(1).innerText).contains('this is a post that I hope somebody will favorite')
|
.expect(getNthStatusContent(1).innerText).contains('this is a post that I hope somebody will favorite')
|
|
@ -0,0 +1,29 @@
|
||||||
|
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')
|
||||||
|
})
|
|
@ -0,0 +1,127 @@
|
||||||
|
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')
|
||||||
|
})
|
|
@ -0,0 +1,94 @@
|
||||||
|
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')
|
||||||
|
})
|
|
@ -0,0 +1,67 @@
|
||||||
|
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')
|
||||||
|
})
|
|
@ -0,0 +1,32 @@
|
||||||
|
import {
|
||||||
|
settingsNavButton,
|
||||||
|
homeNavButton,
|
||||||
|
disableInfiniteScroll,
|
||||||
|
getFirstVisibleStatus,
|
||||||
|
getUrl,
|
||||||
|
showMoreButton, getNthStatusContent
|
||||||
|
} from '../utils'
|
||||||
|
import { loginAsFoobar } from '../roles'
|
||||||
|
import { Selector as $ } from 'testcafe'
|
||||||
|
import { postAs } from '../serverActions'
|
||||||
|
|
||||||
|
fixture`128-disable-infinite-load.js`
|
||||||
|
.page`http://localhost:4002`
|
||||||
|
|
||||||
|
test('Can disable loading items at top of timeline', async t => {
|
||||||
|
await loginAsFoobar(t)
|
||||||
|
await t.click(settingsNavButton)
|
||||||
|
.click($('a').withText('General'))
|
||||||
|
.click(disableInfiniteScroll)
|
||||||
|
.expect(disableInfiniteScroll.checked).ok()
|
||||||
|
.click(homeNavButton)
|
||||||
|
.expect(getUrl()).eql('http://localhost:4002/')
|
||||||
|
.expect(getFirstVisibleStatus().exists).ok()
|
||||||
|
await postAs('admin', 'hey hey hey this is new')
|
||||||
|
await t
|
||||||
|
.expect(showMoreButton.innerText).contains('Show 1 more', {
|
||||||
|
timeout: 20000
|
||||||
|
})
|
||||||
|
.click(showMoreButton)
|
||||||
|
.expect(getNthStatusContent(1).innerText).contains('hey hey hey this is new')
|
||||||
|
})
|
|
@ -0,0 +1,36 @@
|
||||||
|
import {
|
||||||
|
settingsNavButton,
|
||||||
|
homeNavButton,
|
||||||
|
disableUnreadNotifications,
|
||||||
|
getFirstVisibleStatus,
|
||||||
|
getUrl,
|
||||||
|
notificationsNavButton, getTitleText, sleep
|
||||||
|
} from '../utils'
|
||||||
|
import { loginAsFoobar } from '../roles'
|
||||||
|
import { Selector as $ } from 'testcafe'
|
||||||
|
import { postAs } from '../serverActions'
|
||||||
|
|
||||||
|
fixture`129-wellness.js`
|
||||||
|
.page`http://localhost:4002`
|
||||||
|
|
||||||
|
test('Can disable unread notification counts', async t => {
|
||||||
|
await loginAsFoobar(t)
|
||||||
|
await t.click(settingsNavButton)
|
||||||
|
.click($('a').withText('Wellness'))
|
||||||
|
.click(disableUnreadNotifications)
|
||||||
|
.expect(disableUnreadNotifications.checked).ok()
|
||||||
|
.click(homeNavButton)
|
||||||
|
.expect(getUrl()).eql('http://localhost:4002/')
|
||||||
|
.expect(getFirstVisibleStatus().exists).ok()
|
||||||
|
await postAs('admin', 'hey @foobar')
|
||||||
|
await sleep(2000)
|
||||||
|
await t
|
||||||
|
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications')
|
||||||
|
.expect(getTitleText()).notContains('(1)')
|
||||||
|
.click(settingsNavButton)
|
||||||
|
.click($('a').withText('Wellness'))
|
||||||
|
.click(disableUnreadNotifications)
|
||||||
|
.expect(disableUnreadNotifications.checked).notOk()
|
||||||
|
.expect(notificationsNavButton.getAttribute('aria-label')).eql('Notifications (1 notification)')
|
||||||
|
.expect(getTitleText()).contains('(1)')
|
||||||
|
})
|
|
@ -22,8 +22,9 @@ export const composeButton = $('.compose-box-button')
|
||||||
export const composeLengthIndicator = $('.compose-box-length')
|
export const composeLengthIndicator = $('.compose-box-length')
|
||||||
export const emojiButton = $('.compose-box-toolbar button:first-child')
|
export const emojiButton = $('.compose-box-toolbar button:first-child')
|
||||||
export const mediaButton = $('.compose-box-toolbar button:nth-child(2)')
|
export const mediaButton = $('.compose-box-toolbar button:nth-child(2)')
|
||||||
export const postPrivacyButton = $('.compose-box-toolbar button:nth-child(3)')
|
export const pollButton = $('.compose-box-toolbar button:nth-child(3)')
|
||||||
export const contentWarningButton = $('.compose-box-toolbar button:nth-child(4)')
|
export const postPrivacyButton = $('.compose-box-toolbar button:nth-child(4)')
|
||||||
|
export const contentWarningButton = $('.compose-box-toolbar button:nth-child(5)')
|
||||||
export const emailInput = $('input#user_email')
|
export const emailInput = $('input#user_email')
|
||||||
export const passwordInput = $('input#user_password')
|
export const passwordInput = $('input#user_password')
|
||||||
export const authorizeInput = $('button[type=submit]:not(.negative)')
|
export const authorizeInput = $('button[type=submit]:not(.negative)')
|
||||||
|
@ -47,16 +48,25 @@ export const generalSettingsButton = $('a[href="/settings/general"]')
|
||||||
export const markMediaSensitiveInput = $('#choice-mark-media-sensitive')
|
export const markMediaSensitiveInput = $('#choice-mark-media-sensitive')
|
||||||
export const neverMarkMediaSensitiveInput = $('#choice-never-mark-media-sensitive')
|
export const neverMarkMediaSensitiveInput = $('#choice-never-mark-media-sensitive')
|
||||||
export const removeEmojiFromDisplayNamesInput = $('#choice-omit-emoji-in-display-names')
|
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 dialogOptionsOption = $(`.modal-dialog button`)
|
||||||
export const emojiSearchInput = $('.emoji-mart-search input')
|
export const emojiSearchInput = $('.emoji-mart-search input')
|
||||||
export const confirmationDialogOKButton = $('.confirmation-dialog-form-flex button:nth-child(1)')
|
export const confirmationDialogOKButton = $('.confirmation-dialog-form-flex button:nth-child(1)')
|
||||||
export const confirmationDialogCancelButton = $('.confirmation-dialog-form-flex button:nth-child(2)')
|
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 composeModalInput = $('.modal-dialog .compose-box-input')
|
||||||
export const composeModalComposeButton = $('.modal-dialog .compose-box-button')
|
export const composeModalComposeButton = $('.modal-dialog .compose-box-button')
|
||||||
export const composeModalContentWarningInput = $('.modal-dialog .content-warning-input')
|
export const composeModalContentWarningInput = $('.modal-dialog .content-warning-input')
|
||||||
export const composeModalEmojiButton = $('.modal-dialog .compose-box-toolbar button:nth-child(1)')
|
export const composeModalEmojiButton = $('.modal-dialog .compose-box-toolbar button:nth-child(1)')
|
||||||
export const composeModalPostPrivacyButton = $('.modal-dialog .compose-box-toolbar button:nth-child(3)')
|
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 postPrivacyDialogButtonUnlisted = $('[aria-label="Post privacy dialog"] li:nth-child(2) button')
|
export const postPrivacyDialogButtonUnlisted = $('[aria-label="Post privacy dialog"] li:nth-child(2) button')
|
||||||
|
|
||||||
|
@ -64,11 +74,19 @@ export const accountProfileFilterStatuses = $('.account-profile-filters li:nth-c
|
||||||
export const accountProfileFilterStatusesAndReplies = $('.account-profile-filters li:nth-child(2)')
|
export const accountProfileFilterStatusesAndReplies = $('.account-profile-filters li:nth-child(2)')
|
||||||
export const accountProfileFilterMedia = $('.account-profile-filters li:nth-child(3)')
|
export const accountProfileFilterMedia = $('.account-profile-filters li:nth-child(3)')
|
||||||
|
|
||||||
export const notificationFiltersAll = $('.notification-filters li:nth-child(1)')
|
export const notificationsTabAll = $('.notification-filters li:nth-child(1)')
|
||||||
export const notificationFiltersMention = $('.notification-filters li:nth-child(2)')
|
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 function getComposeModalNthMediaAltInput (n) {
|
export function getComposeModalNthMediaAltInput (n) {
|
||||||
return $(`.modal-dialog .compose-media:nth-child(${n}) .compose-media-alt input`)
|
return $(`.modal-dialog .compose-media:nth-child(${n}) .compose-media-alt textarea`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getComposeModalNthMediaImg (n) {
|
export function getComposeModalNthMediaImg (n) {
|
||||||
|
@ -105,6 +123,10 @@ export const getActiveElementRectTop = exec(() => (
|
||||||
(document.activeElement && document.activeElement.getBoundingClientRect().top) || -1
|
(document.activeElement && document.activeElement.getBoundingClientRect().top) || -1
|
||||||
))
|
))
|
||||||
|
|
||||||
|
export const getActiveElementAriaPosInSet = exec(() => (
|
||||||
|
(document.activeElement && document.activeElement.getAttribute('aria-posinset')) || ''
|
||||||
|
))
|
||||||
|
|
||||||
export const getActiveElementInsideNthStatus = exec(() => {
|
export const getActiveElementInsideNthStatus = exec(() => {
|
||||||
let element = document.activeElement
|
let element = document.activeElement
|
||||||
while (element) {
|
while (element) {
|
||||||
|
@ -197,7 +219,7 @@ export const getScrollTop = exec(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
export function getNthMediaAltInput (n) {
|
export function getNthMediaAltInput (n) {
|
||||||
return $(`.compose-box .compose-media:nth-child(${n}) .compose-media-alt input`)
|
return $(`.compose-box .compose-media:nth-child(${n}) .compose-media-alt textarea`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNthComposeReplyInput (n) {
|
export function getNthComposeReplyInput (n) {
|
||||||
|
@ -209,7 +231,39 @@ export function getNthComposeReplyButton (n) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNthPostPrivacyButton (n) {
|
export function getNthPostPrivacyButton (n) {
|
||||||
return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(3)`)
|
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})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNthAutosuggestionResult (n) {
|
export function getNthAutosuggestionResult (n) {
|
||||||
|
@ -293,11 +347,11 @@ export function getNthReplyContentWarningInput (n) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNthReplyContentWarningButton (n) {
|
export function getNthReplyContentWarningButton (n) {
|
||||||
return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(4)`)
|
return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(5)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNthReplyPostPrivacyButton (n) {
|
export function getNthReplyPostPrivacyButton (n) {
|
||||||
return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(3)`)
|
return $(`${getNthStatusSelector(n)} .compose-box-toolbar button:nth-child(4)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNthPostPrivacyOptionInDialog (n) {
|
export function getNthPostPrivacyOptionInDialog (n) {
|
||||||
|
@ -382,16 +436,20 @@ export async function validateTimeline (t, timeline) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function scrollToStatus (t, n) {
|
export async function scrollToStatus (t, n) {
|
||||||
|
return scrollFromStatusToStatus(t, 1, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function scrollFromStatusToStatus (t, start, end) {
|
||||||
let timeout = 20000
|
let timeout = 20000
|
||||||
for (let i = 1; i < n; i++) {
|
for (let i = start; i < end; i++) {
|
||||||
await t.expect(getNthStatus(i).exists).ok({ timeout })
|
await t.expect(getNthStatus(i).exists).ok({ timeout })
|
||||||
.hover(getNthStatus(i))
|
.hover(getNthStatus(i))
|
||||||
.expect($('.loading-footer').exist).notOk({ timeout })
|
|
||||||
.expect($(`${getNthStatusSelector(i)} .status-toolbar`).exists).ok({ timeout })
|
.expect($(`${getNthStatusSelector(i)} .status-toolbar`).exists).ok({ timeout })
|
||||||
.hover($(`${getNthStatusSelector(i)} .status-toolbar`))
|
.hover($(`${getNthStatusSelector(i)} .status-toolbar`))
|
||||||
.expect($('.loading-footer').exist).notOk({ timeout })
|
|
||||||
}
|
}
|
||||||
await t.hover(getNthStatus(n))
|
await t
|
||||||
|
.expect(getNthStatus(end).exists).ok({ timeout })
|
||||||
|
.hover(getNthStatus(end))
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clickToNotificationsAndBackHome (t) {
|
export async function clickToNotificationsAndBackHome (t) {
|
||||||
|
|
Loading…
Reference in New Issue