kopia lustrzana https://github.com/nolanlawson/pinafore
Porównaj commity
42 Commity
Autor | SHA1 | Data |
---|---|---|
Nolan Lawson | 8f61ea75ce | |
Nolan Lawson | 5889b404cb | |
Nolan Lawson | 794d9ca74e | |
Nolan Lawson | 72a07ac40d | |
Nolan Lawson | ed9a9f6539 | |
Arnaldo Gabriel | 452b34b3b4 | |
Thomas Preece | fd4bb4d864 | |
vitalyster | c426b7fe31 | |
Noelia Ruiz Martínez | c2851ce104 | |
Nolan Lawson | 2578d0964d | |
Noelia Ruiz Martínez | ff53fcab10 | |
Nolan Lawson | 750235cd8f | |
Nolan Lawson | b5cad87aaf | |
Nick Colley | a85ff62d48 | |
Nick Colley | e06f63684e | |
Nick Colley | f81778d37f | |
Nick Colley | 746298a1f7 | |
Nolan Lawson | 02f1dad098 | |
Nick Colley | 3edfed971f | |
Noelia Ruiz Martínez | d71430f86d | |
Noelia Ruiz Martínez | 6124c948de | |
Nolan Lawson | 774aa7a21c | |
Nolan Lawson | 276c6e7bea | |
Nolan Lawson | f61054a3d5 | |
Nolan Lawson | b1dc43a9c9 | |
Nolan Lawson | 040462f5b5 | |
Thomas Broyer | f5f3395a53 | |
Nick Colley | 3fb152ac7c | |
Daniel Soohan Park | 97e3b04f1f | |
Scott Feeney | 3c32b48e29 | |
Noelia Ruiz Martínez | 4a6907bbdc | |
Thomas Broyer | d31c800806 | |
Nolan Lawson | 380d2a0d45 | |
Nolan Lawson | 7fdbd72f13 | |
dependabot[bot] | 62b30f6d99 | |
Nolan Lawson | 6d6eb59f41 | |
James Teh | 30b00667f2 | |
Nick Colley | da28e98cfb | |
Nick Colley | 7417e89f78 | |
James Teh | 815438172e | |
James Teh | 8fc9d5c728 | |
Scott Feeney | a775bd9193 |
|
@ -4,7 +4,7 @@ on:
|
|||
branches: [ master ]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.2
|
||||
|
@ -28,7 +28,7 @@ jobs:
|
|||
node-version: '14'
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.0.3'
|
||||
ruby-version: '3.0.4'
|
||||
- name: Cache Mastodon bundler
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
|
|
|
@ -4,7 +4,7 @@ on:
|
|||
branches: [ master ]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-20.04
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.2
|
||||
|
@ -28,7 +28,7 @@ jobs:
|
|||
node-version: '14'
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: '3.0.3'
|
||||
ruby-version: '3.0.4'
|
||||
- name: Cache Mastodon bundler
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
|
|
|
@ -38,7 +38,7 @@ running on `localhost:3000`.
|
|||
### Running integration tests
|
||||
|
||||
The integration tests require running Mastodon itself,
|
||||
meaning the [Mastodon development guide](https://docs.joinmastodon.org/development/overview/)
|
||||
meaning the [Mastodon development guide](https://docs.joinmastodon.org/dev/setup/)
|
||||
is relevant here. In particular, you'll need a recent
|
||||
version of Ruby, Redis, and Postgres running. For a full list of deps, see `bin/setup-mastodon-in-travis.sh`.
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
# Pinafore
|
||||
# Pinafore [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/)
|
||||
|
||||
_**Note:** Pinafore is unmaintained. Read [this](https://nolanlawson.com/2023/01/09/retiring-pinafore/). Original documentation follows._
|
||||
|
||||
An alternative web client for [Mastodon](https://joinmastodon.org), focused on speed and simplicity.
|
||||
|
||||
|
|
|
@ -21,15 +21,8 @@ const JSON_TEMPLATE = {
|
|||
github: {
|
||||
silent: true
|
||||
},
|
||||
builds: [
|
||||
{
|
||||
src: 'package.json',
|
||||
use: '@now/static-build',
|
||||
config: {
|
||||
distDir: '__sapper__/export'
|
||||
}
|
||||
}
|
||||
],
|
||||
buildCommand: 'yarn build',
|
||||
outputDirectory: '__sapper__/export',
|
||||
routes: [
|
||||
{
|
||||
src: '^/service-worker\\.js$',
|
||||
|
@ -51,7 +44,13 @@ const JSON_TEMPLATE = {
|
|||
}
|
||||
},
|
||||
{
|
||||
src: '^/.*\\.(png|css|json|svg|jpe?g|map|txt|gz|webapp|woff|woff2)$',
|
||||
src: '^/.*\\.(png|jpe?g)$',
|
||||
headers: {
|
||||
'cache-control': 'public,max-age=31536000,immutable'
|
||||
}
|
||||
},
|
||||
{
|
||||
src: '^/.*\\.(css|json|svg|map|txt|gz|webapp|woff|woff2)$',
|
||||
headers: {
|
||||
'cache-control': 'public,max-age=3600'
|
||||
}
|
||||
|
|
|
@ -4,12 +4,14 @@ import { DEFAULT_LOCALE, LOCALE } from '../src/routes/_static/intl.js'
|
|||
import enUS from '../src/intl/en-US.js'
|
||||
import fr from '../src/intl/fr.js'
|
||||
import de from '../src/intl/de.js'
|
||||
import es from '../src/intl/es.js'
|
||||
|
||||
// TODO: make it so we don't have to explicitly list these out
|
||||
const locales = {
|
||||
'en-US': enUS,
|
||||
fr,
|
||||
de
|
||||
de,
|
||||
es
|
||||
}
|
||||
|
||||
const intl = locales[LOCALE]
|
||||
|
|
|
@ -43,8 +43,8 @@ async function setupMastodonDatabase () {
|
|||
async function installMastodonDependencies () {
|
||||
const cwd = mastodonDir
|
||||
const installCommands = [
|
||||
'gem update --system',
|
||||
'gem install bundler foreman',
|
||||
'gem install bundler -v 2.3.26 --no-document',
|
||||
'gem install foreman -v 0.87.2 --no-document',
|
||||
'bundle config set --local frozen \'true\'',
|
||||
'bundle install',
|
||||
'yarn --pure-lockfile'
|
||||
|
|
|
@ -19,9 +19,9 @@ BIND=0.0.0.0
|
|||
`
|
||||
|
||||
export const GIT_URL = 'https://github.com/tootsuite/mastodon.git'
|
||||
export const GIT_TAG = 'v3.5.3'
|
||||
export const GIT_TAG = 'v4.0.2'
|
||||
|
||||
export const RUBY_VERSION = '3.0.3'
|
||||
export const RUBY_VERSION = '3.0.4'
|
||||
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
||||
export const mastodonDir = path.join(__dirname, '../mastodon')
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export default [
|
||||
{ id: 'pinafore-logo', src: 'src/static/sailboat.svg', inline: true },
|
||||
{ id: 'fa-arrow-left', src: 'src/thirdparty/font-awesome-svg-png/white/svg/arrow-left.svg' },
|
||||
{ id: 'fa-bell', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell.svg', inline: true },
|
||||
{ id: 'fa-bell-o', src: 'src/thirdparty/font-awesome-svg-png/white/svg/bell-o.svg' },
|
||||
{ id: 'fa-users', src: 'src/thirdparty/font-awesome-svg-png/white/svg/users.svg', inline: true },
|
||||
|
|
|
@ -16,5 +16,4 @@ or
|
|||
|
||||
LOCALE=fr yarn dev
|
||||
|
||||
There is also an experimental `LOCALE_DIRECTION` environment variable for the direction (LTR versus RTL) which is
|
||||
exposed to the source code while building.
|
||||
To host a localized version of Pinafore using Vercel, you can see this example: [buildCommand in vercel.json for Spanish](https://github.com/nvdaes/vercelPinafore/blob/45c70fb2088fe5f2380a729dab83e6f3ab4e6291/vercel.json#L9).
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"name": "pinafore",
|
||||
"description": "Alternative web client for Mastodon",
|
||||
"version": "2.4.0",
|
||||
"version": "2.6.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || ^16.0.0 || ^18.0.0"
|
||||
"node": "^12.20.0 || ^14.13.1 || ^16.0.0 || ^18.0.0 || ^20.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "standard && standard --plugin html 'src/routes/**/*.html'",
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
*/
|
||||
img, svg, video,
|
||||
input[type="checkbox"], input[type="radio"],
|
||||
.inline-emoji, .theme-preview {
|
||||
.inline-emoji, .theme-preview, .account-profile {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -188,8 +188,6 @@ export default {
|
|||
}`,
|
||||
pinPage: 'Hefte {label} an',
|
||||
// Status composition
|
||||
overLimit: '{count} {count, plural, =1 {Zeichen} other {Zeichen}} über der Beschränkung',
|
||||
underLimit: '{count} {count, plural, =1 {Zeichen} other {Zeichen}} übrig',
|
||||
composeStatus: 'Tröt erstellen',
|
||||
postStatus: 'Tröt!',
|
||||
contentWarning: 'Inhaltswarnung',
|
||||
|
|
|
@ -153,6 +153,8 @@ export default {
|
|||
<li><kbd>f</kbd> to favorite</li>
|
||||
<li><kbd>b</kbd> to boost</li>
|
||||
<li><kbd>r</kbd> to reply</li>
|
||||
<li><kbd>Escape</kbd> to close reply</li>
|
||||
<li><kbd>a</kbd> to bookmark</li>
|
||||
<li><kbd>i</kbd> to open images, video, or audio</li>
|
||||
<li><kbd>y</kbd> to show or hide sensitive media</li>
|
||||
<li><kbd>m</kbd> to mention the author</li>
|
||||
|
@ -192,8 +194,6 @@ export default {
|
|||
}`,
|
||||
pinPage: 'Pin {label}',
|
||||
// Status composition
|
||||
overLimit: '{count} {count, plural, =1 {character} other {characters}} over limit',
|
||||
underLimit: '{count} {count, plural, =1 {character} other {characters}} remaining',
|
||||
composeStatus: 'Compose toot',
|
||||
postStatus: 'Toot!',
|
||||
contentWarning: 'Content warning',
|
||||
|
@ -205,7 +205,7 @@ export default {
|
|||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
description: 'Description',
|
||||
descriptionLabel: 'Describe for the visually impaired (image, video) or auditorily impaired (audio, video)',
|
||||
descriptionLabel: 'Describe for visually impaired (image, video) or auditorily impaired (audio, video) people',
|
||||
markAsSensitive: 'Mark media as sensitive',
|
||||
// Polls
|
||||
createPoll: 'Create poll',
|
||||
|
@ -229,7 +229,7 @@ export default {
|
|||
postPrivacyLabel: 'Adjust privacy (currently {label})',
|
||||
addContentWarning: 'Add content warning',
|
||||
removeContentWarning: 'Remove content warning',
|
||||
altLabel: 'Describe for the visually impaired',
|
||||
altLabel: 'Describe for visually impaired people',
|
||||
extractText: 'Extract text from image',
|
||||
extractingText: 'Extracting text…',
|
||||
extractingTextCompletion: 'Extracting text ({percent}% complete)…',
|
||||
|
@ -368,6 +368,7 @@ export default {
|
|||
general: 'General',
|
||||
generalSettings: 'General settings',
|
||||
showSensitive: 'Show sensitive media by default',
|
||||
showAllSpoilers: 'Expand content warnings by default',
|
||||
showPlain: 'Show a plain gray color for sensitive media',
|
||||
allSensitive: 'Treat all media as sensitive',
|
||||
largeMedia: 'Show large inline images and videos',
|
||||
|
@ -497,6 +498,8 @@ export default {
|
|||
}: {description}`,
|
||||
accountFollowedYou: '{name} followed you, {account}',
|
||||
accountSignedUp: '{name} signed up, {account}',
|
||||
accountRequestedFollow: '{name} requested to follow you, {account}',
|
||||
accountReported: '{name} filed a report, {account}',
|
||||
reblogCountsHidden: 'Boost counts hidden',
|
||||
favoriteCountsHidden: 'Favorite counts hidden',
|
||||
rebloggedTimes: `Boosted {count, plural,
|
||||
|
@ -511,6 +514,9 @@ export default {
|
|||
rebloggedYou: 'boosted your toot',
|
||||
favoritedYou: 'favorited your toot',
|
||||
followedYou: 'followed you',
|
||||
edited: 'edited their toot',
|
||||
requestedFollow: 'requested to follow you',
|
||||
reported: 'filed a report',
|
||||
signedUp: 'signed up',
|
||||
posted: 'posted',
|
||||
pollYouCreatedEnded: 'A poll you created has ended',
|
||||
|
@ -526,6 +532,7 @@ export default {
|
|||
// Accessible status labels
|
||||
accountRebloggedYou: '{account} boosted your toot',
|
||||
accountFavoritedYou: '{account} favorited your toot',
|
||||
accountEdited: '{account} edited their toot',
|
||||
rebloggedByAccount: 'Boosted by {account}',
|
||||
contentWarningContent: 'Content warning: {spoiler}',
|
||||
hasMedia: 'has media',
|
||||
|
|
|
@ -0,0 +1,696 @@
|
|||
export default {
|
||||
// Home page, basic <title> and <description>
|
||||
appName: 'Pinafore',
|
||||
appDescription: 'Un cliente web alternativo para Mastodon, centrado en la velocidad y la sencillez.',
|
||||
homeDescription: `
|
||||
<p>
|
||||
Pinafore es un cliente web para
|
||||
<a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>,
|
||||
diseñado para ser rápido y sencillo.
|
||||
</p>
|
||||
<p>
|
||||
Lee el
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://nolanlawson.com/2018/04/09/introducing-pinafore-for-mastodon/">artículo introductorio en el blog</a>,
|
||||
o comienza iniciando sesión en una instancia:
|
||||
</p>`,
|
||||
logIn: 'Iniciar sesión',
|
||||
footer: `
|
||||
<p>
|
||||
Pinafore es
|
||||
<a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">software de código abierto</a>
|
||||
creado por
|
||||
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
|
||||
y distribuido bajo la
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">Licencia AGPL</a>.
|
||||
Aquí está la <a href="/settings/about#privacy-policy" rel="prefetch">política de privacidad</a>.
|
||||
</p>
|
||||
`,
|
||||
// Manifest
|
||||
longAppName: 'Pinafore para Mastodon',
|
||||
newStatus: 'Nuevo toot',
|
||||
// Generic UI
|
||||
loading: 'Cargando',
|
||||
okay: 'OK',
|
||||
cancel: 'Cancelar',
|
||||
alert: 'Alerta',
|
||||
close: 'Cerrar',
|
||||
error: 'Error: {error}',
|
||||
errorShort: 'Error:',
|
||||
// Relative timestamps
|
||||
justNow: 'ahora mismo',
|
||||
// Navigation, page titles
|
||||
navItemLabel: `
|
||||
{label} {selected, select,
|
||||
true {(página actual)}
|
||||
other {}
|
||||
} {name, select,
|
||||
notifications {{count, plural,
|
||||
=0 {}
|
||||
one {(1 notificación)}
|
||||
other {({count} notificaciones)}
|
||||
}}
|
||||
community {{count, plural,
|
||||
=0 {}
|
||||
one {(1 solicitud de seguimiento)}
|
||||
other {({count} solicitudes de seguimiento)}
|
||||
}}
|
||||
other {}
|
||||
}
|
||||
`,
|
||||
blockedUsers: 'Usuarios bloqueados',
|
||||
bookmarks: 'Marcadores',
|
||||
directMessages: 'Mensajes directos',
|
||||
favorites: 'Favoritos',
|
||||
federated: 'Federada',
|
||||
home: 'Inicio',
|
||||
local: 'Local',
|
||||
notifications: 'Notificaciones',
|
||||
mutedUsers: 'Usuarios silenciados',
|
||||
pinnedStatuses: 'Toots fijados',
|
||||
followRequests: 'Solicitudes de seguimiento',
|
||||
followRequestsLabel: `Solicitudes de seguimiento {hasFollowRequests, select,
|
||||
true {({count})}
|
||||
other {}
|
||||
}`,
|
||||
list: 'Lista',
|
||||
search: 'Buscar',
|
||||
pageHeader: 'Encabezado de página',
|
||||
goBack: 'Retroceder',
|
||||
back: 'Atrás',
|
||||
profile: 'Perfil',
|
||||
federatedTimeline: 'Cronología federada',
|
||||
localTimeline: 'Cronología local',
|
||||
// community page
|
||||
community: 'Comunidad',
|
||||
pinnableTimelines: 'Cronologías que puedes fijar',
|
||||
timelines: 'Cronologías',
|
||||
lists: 'Listas',
|
||||
instanceSettings: 'Opciones para instancia',
|
||||
notificationMentions: 'Notificación de menciones',
|
||||
profileWithMedia: 'Perfil con multimedia',
|
||||
profileWithReplies: 'Perfil con respuestas',
|
||||
hashtag: 'Hashtag',
|
||||
// not logged in
|
||||
profileNotLoggedIn: 'Aquí se mostrará una cronología de usuario cuando hayas iniciado sesión.',
|
||||
bookmarksNotLoggedIn: 'Tus marcadores se mostrarán aquí cuando hayas iniciado sesión.',
|
||||
directMessagesNotLoggedIn: 'Tus mensajes directos se mostrarán aquí cuando hayas iniciado sesión.',
|
||||
favoritesNotLoggedIn: 'Tus favoritos se mostrarán aquí cuando hayas iniciado sesión.',
|
||||
federatedTimelineNotLoggedIn: 'Tu cronología federada se mostrará aquí cuando hayas iniciado sesión.',
|
||||
localTimelineNotLoggedIn: 'Tu cronología localse mostrará aquí cuando hayas iniciado sesión.',
|
||||
searchNotLoggedIn: 'Puedes buscar una vez que inicias sesión en una instancia.',
|
||||
communityNotLoggedIn: 'Las opciones para comunidad se mostrarán aquí cuando hayas iniciado sesión.',
|
||||
listNotLoggedIn: 'Aquí se mostrará una lista cuando hayas iniciado sesión.',
|
||||
notificationsNotLoggedIn: 'Tus notificaciones se mostrarán aquí cuando hayas iniciado sesión.',
|
||||
notificationMentionsNotLoggedIn: 'Las notificaciones de tus menciones se mostrarán aquí cuando hayas iniciado sesión.',
|
||||
statusNotLoggedIn: 'Aquí se mostrará un hilo de toots cuando hayas iniciado sesión.',
|
||||
tagNotLoggedIn: 'Aquí se mostrará una cronología de hashtags cuando hayas iniciado sesión.',
|
||||
// Notification subpages
|
||||
filters: 'Filtros',
|
||||
all: 'Todo',
|
||||
mentions: 'Menciones',
|
||||
// Follow requests
|
||||
approve: 'Aceptar',
|
||||
reject: 'Rechazar',
|
||||
// Hotkeys
|
||||
hotkeys: 'Atajos de teclado',
|
||||
global: 'Globales',
|
||||
timeline: 'Cronología',
|
||||
media: 'Multimedia',
|
||||
globalHotkeys: `
|
||||
{leftRightChangesFocus, select,
|
||||
true {
|
||||
<li><kbd>→</kbd> para ir al elemento enfocable siguiente</li>
|
||||
<li><kbd>←</kbd> para ir al elemento enfocable anterior</li>
|
||||
}
|
||||
other {}
|
||||
}
|
||||
<li>
|
||||
<kbd>1</kbd> - <kbd>6</kbd>
|
||||
{leftRightChangesFocus, select,
|
||||
true {}
|
||||
other {o <kbd>←</kbd>/<kbd>→</kbd>}
|
||||
}
|
||||
para cambiar de columna
|
||||
</li>
|
||||
<li><kbd>7</kbd> o <kbd>c</kbd> para redactar un nuevo toot</li>
|
||||
<li><kbd>s</kbd> o <kbd>/</kbd> para buscar</li>
|
||||
<li><kbd>g</kbd> + <kbd>h</kbd> para ir a inicio</li>
|
||||
<li><kbd>g</kbd> + <kbd>n</kbd> para ir a notificaciones</li>
|
||||
<li><kbd>g</kbd> + <kbd>l</kbd> to para ir a la cronología local</li>
|
||||
<li><kbd>g</kbd> + <kbd>t</kbd> para ir a la cronología federada</li>
|
||||
<li><kbd>g</kbd> + <kbd>c</kbd> para ir a la página comunidad</li>
|
||||
<li><kbd>g</kbd> + <kbd>d</kbd> para ir a la página de mensajes directos</li>
|
||||
<li><kbd>h</kbd> o <kbd>?</kbd> para abrir o cerrar el diálogo de ayuda</li>
|
||||
<li><kbd>Backspace</kbd> para retroceder, cerrar diálogos</li>
|
||||
`,
|
||||
timelineHotkeys: `
|
||||
<li><kbd>j</kbd> o <kbd>↓</kbd> para activar el toot siguiente</li>
|
||||
<li><kbd>k</kbd> o <kbd>↑</kbd> para activar el toot anterior</li>
|
||||
<li><kbd>.</kbd> para mostrar más y desplazarse al principio</li>
|
||||
<li><kbd>o</kbd> para abrir</li>
|
||||
<li><kbd>f</kbd> para marcar como favorito</li>
|
||||
<li><kbd>b</kbd> para reenviar</li>
|
||||
<li><kbd>r</kbd> para responder</li>
|
||||
<li><kbd>Escape</kbd> para cerrar respuesta</li>
|
||||
<li><kbd>a</kbd> para marcador</li>
|
||||
<li><kbd>i</kbd> para abrir imágenes, vídeo o audio</li>
|
||||
<li><kbd>y</kbd> para mostrar u ocultar multimedia sensible</li>
|
||||
<li><kbd>m</kbd> para mencionar al autor</li>
|
||||
<li><kbd>p</kbd> para abrir el perfil del autor</li>
|
||||
<li><kbd>l</kbd> para abrir el enlace de la publicación en una nueva pestaña</li>
|
||||
<li><kbd>x</kbd> para mostrar u ocultar el texto tras una advertencia de contenido</li>
|
||||
<li><kbd>z</kbd> para mostrar u ocultar todas las advertencias de contenido en un hilo</li>
|
||||
`,
|
||||
mediaHotkeys: `
|
||||
<li><kbd>←</kbd> / <kbd>→</kbd> para ir a siguiente o anterior</li>
|
||||
`,
|
||||
// Community page, tabs
|
||||
tabLabel: `{label} {current, select,
|
||||
true {(Actual)}
|
||||
other {}
|
||||
}`,
|
||||
pageTitle: `
|
||||
{hasNotifications, select,
|
||||
true {({count})}
|
||||
other {}
|
||||
}
|
||||
{name}
|
||||
·
|
||||
{showInstanceName, select,
|
||||
true {{instanceName}}
|
||||
other {Pinafore}
|
||||
}
|
||||
`,
|
||||
pinLabel: `{label} {pinnable, select,
|
||||
true {
|
||||
{pinned, select,
|
||||
true {(página fijada)}
|
||||
other {(Página no fijada)}
|
||||
}
|
||||
}
|
||||
other {}
|
||||
}`,
|
||||
pinPage: 'Fijar {label}',
|
||||
// Status composition
|
||||
composeStatus: 'Redactar toot',
|
||||
postStatus: 'Toot!',
|
||||
contentWarning: 'Advertencia de contenido',
|
||||
dropToUpload: 'Soltar para subir',
|
||||
invalidFileType: 'Tipo de fichero no válido',
|
||||
composeLabel: '¿En qué estás pensando?',
|
||||
autocompleteDescription: 'Cuando haya disponibles resultados de autocompletado, pulsa las flechas arriba o abajo y enter para seleccionar.',
|
||||
mediaUploads: 'Subidas multimedia',
|
||||
edit: 'Editar',
|
||||
delete: 'Borrar',
|
||||
description: 'Descripción',
|
||||
descriptionLabel: 'Describir para las personas con discapacidad visual (imagen, vídeo) o con discapacidad auditiva (audio, vídeo)',
|
||||
markAsSensitive: 'Marcar multimedia como sensible',
|
||||
// Polls
|
||||
createPoll: 'Crear encuesta',
|
||||
removePollChoice: 'Eliminar opción {index}',
|
||||
pollChoiceLabel: 'Opción {index}',
|
||||
multipleChoice: 'Selección múltiple',
|
||||
pollDuration: 'Duración de la encuesta',
|
||||
fiveMinutes: '5 minutos',
|
||||
thirtyMinutes: '30 minutos',
|
||||
oneHour: '1 hora',
|
||||
sixHours: '6 horas',
|
||||
twelveHours: '12 horas',
|
||||
oneDay: '1 día',
|
||||
threeDays: '3 días',
|
||||
sevenDays: '7 días',
|
||||
never: 'Nunca',
|
||||
addEmoji: 'Insertar emoji',
|
||||
addMedia: 'Añadir multimedia (imágenes, vídeo, audio)',
|
||||
addPoll: 'Añadir encuesta',
|
||||
removePoll: 'Eliminar encuesta',
|
||||
postPrivacyLabel: 'Ajustar privacidad (actualmente {label})',
|
||||
addContentWarning: 'Añadir advertencia de contenido',
|
||||
removeContentWarning: 'Eliminar advertencia de contenido',
|
||||
altLabel: 'Describir para las personas con discapacidad visual',
|
||||
extractText: 'Extraer texto de imagen',
|
||||
extractingText: 'Extrayendo texto…',
|
||||
extractingTextCompletion: 'Extrayendo texto ({percent}% completado)…',
|
||||
unableToExtractText: 'No se puede extraer texto.',
|
||||
// Account options
|
||||
followAccount: 'Seguir a {account}',
|
||||
unfollowAccount: 'Dejar de seguir a {account}',
|
||||
blockAccount: 'Bloquear a {account}',
|
||||
unblockAccount: 'Desbloquear a {account}',
|
||||
muteAccount: 'Silenciar a {account}',
|
||||
unmuteAccount: 'Dejar de silenciar a Unmute {account}',
|
||||
showReblogsFromAccount: 'Mostrar toots reenviados por {account}',
|
||||
hideReblogsFromAccount: 'Ocultar toots reenviados por {account}',
|
||||
showDomain: 'Dejar de ocultar {domain}',
|
||||
hideDomain: 'Ocultar {domain}',
|
||||
reportAccount: 'Denunciar a {account}',
|
||||
mentionAccount: 'Mencionar a {account}',
|
||||
copyLinkToAccount: 'Copiar enlace a cuenta',
|
||||
copiedToClipboard: 'Copiado al portapapeles',
|
||||
// Media dialog
|
||||
navigateMedia: 'Navegar por elementos multimedia',
|
||||
showPreviousMedia: 'Mostrar multimedia anterior',
|
||||
showNextMedia: 'Mostrar multimedia siguiente',
|
||||
enterPinchZoom: 'Modo pinch-zoom',
|
||||
exitPinchZoom: 'Salir del modo pinch-zoom',
|
||||
showMedia: `Mostrar {index, select,
|
||||
1 {primer}
|
||||
2 {segundo}
|
||||
3 {tercero}
|
||||
other {cuarto}
|
||||
} multimedia {current, select,
|
||||
true {(actual)}
|
||||
other {}
|
||||
}`,
|
||||
previewFocalPoint: 'Previsualizar (punto focal)',
|
||||
enterFocalPoint: 'Introducir el punto focal (X, Y) para este multimedia',
|
||||
muteNotifications: 'Silenciar también las notificaciones',
|
||||
muteAccountConfirm: '¿Silenciar a {account}?',
|
||||
mute: 'Silenciar',
|
||||
unmute: 'Dejar de silenciar',
|
||||
zoomOut: 'Alejar',
|
||||
zoomIn: 'Acercar',
|
||||
// Reporting
|
||||
reportingLabel: 'Estás denunciando a {account} a los moderadores de {instance}.',
|
||||
additionalComments: 'Comentarios adicionales',
|
||||
forwardDescription: '?Reenviar también a los moderadores de {instance}?',
|
||||
forwardLabel: 'Reenviar a {instance}',
|
||||
unableToLoadStatuses: 'No se pueden cargar los toots recientes: {error}',
|
||||
report: 'Denunciar',
|
||||
noContent: '(Sin contenido)',
|
||||
noStatuses: 'No hay toots para denunciar',
|
||||
// Status options
|
||||
unpinFromProfile: 'Dejar de fijar en el perfil',
|
||||
pinToProfile: 'Fijar en el perfil',
|
||||
muteConversation: 'Silenciar conversación',
|
||||
unmuteConversation: 'Dejar de silenciar conversación',
|
||||
bookmarkStatus: 'Poner marcador al toot',
|
||||
unbookmarkStatus: 'Quitar marcador al toot',
|
||||
deleteAndRedraft: 'Borrar y volver a redactar',
|
||||
reportStatus: 'Denunciar toot',
|
||||
shareStatus: 'Compartir toot',
|
||||
copyLinkToStatus: 'Copiar enlace al toot',
|
||||
// Account profile
|
||||
profileForAccount: 'Perfil para {account}',
|
||||
statisticsAndMoreOptions: 'Estadísticas y más opciones',
|
||||
statuses: 'Toots',
|
||||
follows: 'Siguiendo',
|
||||
followers: 'Seguidores',
|
||||
moreOptions: 'Más opciones',
|
||||
followersLabel: 'Te han seguido {count}',
|
||||
followingLabel: 'Has seguido a {count}',
|
||||
followLabel: `Seguimiento {requested, select,
|
||||
true {(solicitud de seguimiento)}
|
||||
other {}
|
||||
}`,
|
||||
unfollowLabel: `Dejar de seguir {requested, select,
|
||||
true {(solicitud de seguimiento)}
|
||||
other {}
|
||||
}`,
|
||||
notify: 'Suscribirse a {account}',
|
||||
denotify: 'Cancelar suscripción a {account}',
|
||||
subscribedAccount: 'Te has suscrito a la cuenta',
|
||||
unsubscribedAccount: 'Has cancelado tu suscripción a la cuenta',
|
||||
unblock: 'Desbloquear',
|
||||
nameAndFollowing: 'Nombre y seguimientos',
|
||||
clickToSeeAvatar: 'Haz clic para ver el avatar',
|
||||
opensInNewWindow: '{label} (Se abre en nueva ventana)',
|
||||
blocked: 'Bloqueado',
|
||||
domainHidden: 'Dominio oculto',
|
||||
muted: 'Silenciado',
|
||||
followsYou: 'Te está siguiendo',
|
||||
avatarForAccount: 'Avatar para {account}',
|
||||
fields: 'Campos',
|
||||
accountHasMoved: '{account} se ha trasladado:',
|
||||
profilePageForAccount: 'Página de perfil para {account}',
|
||||
// About page
|
||||
about: 'Acerca de',
|
||||
aboutApp: 'Acerca de Pinafore',
|
||||
aboutAppDescription: `
|
||||
<p>
|
||||
Pinafore es
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore">software libre y de código abierto</a>
|
||||
creado por
|
||||
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Nolan Lawson</a>
|
||||
y distribuido bajo la
|
||||
<a rel="noopener" target="_blank"
|
||||
href="https://github.com/nolanlawson/pinafore/blob/master/LICENSE">GNU Affero General Public License</a>.
|
||||
</p>
|
||||
|
||||
<h2 id="privacy-policy">Política de privacidad</h2>
|
||||
|
||||
<p>
|
||||
Pinafore no almacena ninguna información personal en sus servidores,
|
||||
incluyendo, pero no limitándose a nombres, direcciones de correo electrónico,
|
||||
direcciones IP, posts y fotos.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Pinafore es un sitio estático. Todos los datos son almacenados en tu navegador y compartidos con las instancias del fediverso
|
||||
a las que te conectas.
|
||||
</p>
|
||||
|
||||
<h2>Créditos</h2>
|
||||
|
||||
<p>
|
||||
Iconos proporcionados por <a rel="noopener" target="_blank" href="http://fontawesome.io/">Font Awesome</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Logo gracias a "sailboat" por Gregor Cresnar, de
|
||||
<a rel="noopener" target="_blank" href="https://thenounproject.com/">the Noun Project</a>.
|
||||
</p>`,
|
||||
// Settings
|
||||
settings: 'Opciones de configuración',
|
||||
general: 'General',
|
||||
generalSettings: 'Opciones generales',
|
||||
showSensitive: 'Mostrar multimedia sensible por defecto',
|
||||
showPlain: 'Mostrar un color gris liso para multimedia sensible',
|
||||
allSensitive: 'Tratar todo multimedia como sensible',
|
||||
largeMedia: 'Mostrar imágenes y vídeos grandes incrustados',
|
||||
autoplayGifs: 'Reproducir automáticamente GIFs animados',
|
||||
hideCards: 'Ocultar paneles de previsualización de enlaces',
|
||||
underlineLinks: 'Subrayar enlaces en toots y perfiles',
|
||||
accessibility: 'Accesibilidad',
|
||||
reduceMotion: 'Reducir movimiento en animaciones de la interfaz',
|
||||
disableTappable: 'Deshabilitar área para tocar en todo el toot',
|
||||
removeEmoji: 'Eliminar emoji de nombres de usuario',
|
||||
shortAria: 'Usar etiquetas ARIA cortas para artículos',
|
||||
theme: 'Diseño visual',
|
||||
themeForInstance: 'Diseño visual para {instance}',
|
||||
disableCustomScrollbars: 'Deshabilitar barras deslizantes personalizadas',
|
||||
bottomNav: 'Situar la barra de navegación al final de la pantalla',
|
||||
centerNav: 'Centrar la barra de navegación',
|
||||
preferences: 'Preferencias',
|
||||
hotkeySettings: 'Opciones para atajos de teclado',
|
||||
disableHotkeys: 'Deshabilitar todos los atajos de teclado',
|
||||
leftRightArrows: 'Las flechas izquierda/derecha cambian el foco en vez de columnas/multimedia',
|
||||
guide: 'Guía',
|
||||
reload: 'Recargar',
|
||||
// Wellness settings
|
||||
wellness: 'Bienestar',
|
||||
wellnessSettings: 'Opciones para el bienestar',
|
||||
wellnessDescription: `Las opciones para el bienestar están diseñadas para reducir los aspectos que inducen adicción o ansiedad en las redes sociales.
|
||||
Elige cualquier opción que vaya bien para ti.`,
|
||||
enableAll: 'Habilitar todos',
|
||||
metrics: 'Métricas',
|
||||
hideFollowerCount: 'Ocultar recuento de seguidores (hasta 10)',
|
||||
hideReblogCount: 'Ocultar recuento de reenvíos',
|
||||
hideFavoriteCount: 'Ocultar recuento de favoritos',
|
||||
hideUnread: 'Ocultar recuento de notificaciones sin leer (es decir, el punto rojo)',
|
||||
// The quality that makes something seem important or interesting because it seems to be happening now
|
||||
immediacy: 'Inmediatez',
|
||||
showAbsoluteTimestamps: 'Mostrar marcas de tiempo absolutas (p.ej., "3 de marzo") en vez de marcas de tiempo relativas (p. ej., "hace 5 minutos")',
|
||||
ui: 'Interfaz',
|
||||
grayscaleMode: 'Modo escala de grises',
|
||||
wellnessFooter: `Estas opciones están parcialmente basadas en pautas del
|
||||
<a rel="noopener" target="_blank" href="https://humanetech.com">Center for Humane Technology</a>.`,
|
||||
// This is a link: "You can filter or disable notifications in the _instance settings_"
|
||||
filterNotificationsPre: 'Puedes filtrar o deshabilitar notificaciones en',
|
||||
filterNotificationsText: 'opciones para instancia',
|
||||
filterNotificationsPost: '',
|
||||
// Custom tooltips, like "Disable _infinite scroll_", where you can click _infinite scroll_
|
||||
// to see a description. It's hard to properly internationalize, so we just break up the strings.
|
||||
disableInfiniteScrollPre: 'Deshabilitar',
|
||||
disableInfiniteScrollText: 'desplazamiento infinito',
|
||||
disableInfiniteScrollDescription: `Cuando el desplazamiento infinito esté deshabilitado, los nuevos toots no se mostrarán automáticamente al final o al principio de la cronología. En vez de esto, habrá botones que te permitirán
|
||||
cargar más contenido a demanda.`,
|
||||
disableInfiniteScrollPost: '',
|
||||
// Instance settings
|
||||
loggedInAs: 'Iniciaste sesión como',
|
||||
homeTimelineFilters: 'Filtros para la cronología Inicio',
|
||||
notificationFilters: 'Filtros para notificaciones',
|
||||
pushNotifications: 'Notificaciones Push',
|
||||
// Add instance page
|
||||
storageError: `Parece que Pinafore no puede almacenar datos localmente. ¿Está tu navegador en modo privado
|
||||
o bloqueando las cookies? Pinafore almacena todos los datos localmente, y requiere LocalStorage e
|
||||
IndexedDB para funcionar correctamente.`,
|
||||
javaScriptError: 'Debes habilitar JavaScript para iniciar sesión.',
|
||||
enterInstanceName: 'Introducir nombre de instancia',
|
||||
instanceColon: 'Instancia:',
|
||||
// Custom tooltip, concatenated together
|
||||
getAnInstancePre: '¿No tienes una',
|
||||
getAnInstanceText: 'instancia',
|
||||
getAnInstanceDescription: 'Una instancia es tu servidor de inicio de Mastodon, por ejemplo, mastodon.social o cybre.space.',
|
||||
getAnInstancePost: '?',
|
||||
joinMastodon: '¡Unirse a Mastodon!',
|
||||
instancesYouveLoggedInTo: 'Instancias en las que has iniciado sesión:',
|
||||
addAnotherInstance: 'Añadir otra instancia',
|
||||
youreNotLoggedIn: 'No has iniciado sesión en ninguna instancia.',
|
||||
currentInstanceLabel: `{instance} {current, select,
|
||||
true {(instancia actual)}
|
||||
other {}
|
||||
}`,
|
||||
// Link text
|
||||
logInToAnInstancePre: '',
|
||||
logInToAnInstanceText: 'Inicia sesión en una instancia',
|
||||
logInToAnInstancePost: 'para empezar a usar Pinafore.',
|
||||
// Another custom tooltip
|
||||
showRingPre: 'Mostrar siempre',
|
||||
showRingText: 'anillo del foco',
|
||||
showRingDescription: 'El anillo del foco es el contorno que muestra el elemento que actualmente tiene el foco. Por defecto solo se muestra cuando se usa el teclado (no el ratón o un dispositivo táctil), pero puedes elegir mostrarlo siempre.',
|
||||
showRingPost: '',
|
||||
instances: 'Instancias',
|
||||
addInstance: 'Añadir instancia',
|
||||
homeTimelineFilterSettings: 'Opciones para filtros de la cronología Inicio',
|
||||
showReblogs: 'Mostrar reenvíos',
|
||||
showReplies: 'Mostrar respuestas',
|
||||
switchOrLogOut: 'Seleccionar o cerrar sesión en esta instancia',
|
||||
switchTo: 'Seleccionar esta instancia',
|
||||
switchToInstance: 'Seleccionar instancia',
|
||||
switchToNameOfInstance: 'Seleccionar {instance}',
|
||||
logOut: 'Cerrar sesión',
|
||||
logOutOfInstanceConfirm: '¿Cerrar sesión en {instance}?',
|
||||
notificationFilterSettings: 'Opciones para filtros de notificaciones',
|
||||
// Push notifications
|
||||
browserDoesNotSupportPush: 'Tu navegador no admite notificaciones Push.',
|
||||
deniedPush: 'Has denegado el permiso para mostrar notificaciones.',
|
||||
pushNotificationsNote: 'Observa que solo puedes recibir notificaciones Push para una instancia al mismo tiempo.',
|
||||
pushSettings: 'Opciones para notificaciones Push',
|
||||
newFollowers: 'Nuevos seguidores',
|
||||
reblogs: 'Reenvíos',
|
||||
pollResults: 'Resultados de encuesta',
|
||||
subscriptions: 'Suscripción a toots',
|
||||
needToReauthenticate: 'Tienes que volver a autenticarte para habilitar las notificaciones Push. ¿Cerrr sesión en {instance}?',
|
||||
failedToUpdatePush: 'Se ha producido un fallo al actualizar las opciones para notificaciones Push: {error}',
|
||||
// Themes
|
||||
chooseTheme: 'Elegir un diseño visual',
|
||||
darkBackground: 'Fondo oscuro',
|
||||
lightBackground: 'Fondo claro',
|
||||
themeLabel: `{label} {default, select,
|
||||
true {(por defecto)}
|
||||
other {}
|
||||
}`,
|
||||
animatedImage: 'Imagen animada: {description}',
|
||||
showImage: `Mostrar {animated, select,
|
||||
true {animated}
|
||||
other {}
|
||||
} imagen: {description}`,
|
||||
playVideoOrAudio: `Reproducir {audio, select,
|
||||
true {audio}
|
||||
other {vídeo}
|
||||
}: {description}`,
|
||||
accountFollowedYou: '{name} te siguió, {account}',
|
||||
accountSignedUp: '{name} inició sesión, {account}',
|
||||
accountRequestedFollow: '{name} solicitó seguirte, {account}',
|
||||
accountReported: '{name} creó una denuncia, {account}',
|
||||
reblogCountsHidden: 'Recuento de reenvíos oculto',
|
||||
favoriteCountsHidden: 'Recuento de favoritos oculto',
|
||||
rebloggedTimes: `Reenviado {count, plural,
|
||||
one {1 vez}
|
||||
other {{count} veces}
|
||||
}`,
|
||||
favoritedTimes: `Marcado como favorito {count, plural,
|
||||
one {1 vez}
|
||||
other {{count} veces}
|
||||
}`,
|
||||
pinnedStatus: 'Toot fijado',
|
||||
rebloggedYou: 'reenvió tu toot',
|
||||
favoritedYou: 'marcó como favorito tu toot',
|
||||
followedYou: 'te siguió',
|
||||
edited: 'editó su toot',
|
||||
requestedFollow: 'solicitó seguirte',
|
||||
reported: 'creó una denuncia',
|
||||
signedUp: 'sesión iniciada',
|
||||
posted: 'publicado',
|
||||
pollYouCreatedEnded: 'Una encuesta que creaste ha finalizado',
|
||||
pollYouVotedEnded: 'Una encuesta en la que votaste ha finalizado',
|
||||
reblogged: 'reenviado',
|
||||
favorited: 'marcado como favorito',
|
||||
unreblogged: 'no reenviado',
|
||||
unfavorited: 'no marcado como favorito',
|
||||
showSensitiveMedia: 'Mostrar multimedia sensible',
|
||||
hideSensitiveMedia: 'Ocultar multimedia sensible',
|
||||
clickToShowSensitive: 'Contenido sensible. Haz clic para mostrar.',
|
||||
longPost: 'Publicación larga',
|
||||
// Accessible status labels
|
||||
accountRebloggedYou: '{account} reenvió tu toot',
|
||||
accountFavoritedYou: '{account} marcó como favorito tu toot',
|
||||
accountEdited: '{account} editó su toot',
|
||||
rebloggedByAccount: 'reenviado por {account}',
|
||||
contentWarningContent: 'Advertencia de contenido: {spoiler}',
|
||||
hasMedia: 'tiene multimedia',
|
||||
hasPoll: 'tiene encuesta',
|
||||
shortStatusLabel: '{privacy} toot de {account}',
|
||||
// Privacy types
|
||||
public: 'Público',
|
||||
unlisted: 'No listado',
|
||||
followersOnly: 'Solo seguidores',
|
||||
direct: 'Directo',
|
||||
// Themes
|
||||
themeRoyal: 'Royal',
|
||||
themeScarlet: 'Escarlata',
|
||||
themeSeafoam: 'Espuma de mar',
|
||||
themeHotpants: 'Hotpants',
|
||||
themeOaken: 'Roble',
|
||||
themeMajesty: 'Majesty',
|
||||
themeGecko: 'Gecko',
|
||||
themeGrayscale: 'Escala de grises',
|
||||
themeOzark: 'Ozark',
|
||||
themeCobalt: 'Cobalto',
|
||||
themeSorcery: 'Sorcery',
|
||||
themePunk: 'Punk',
|
||||
themeRiot: 'Riot',
|
||||
themeHacker: 'Hacker',
|
||||
themeMastodon: 'Mastodon',
|
||||
themePitchBlack: 'Tono negro',
|
||||
themeDarkGrayscale: 'Escala de gris oscuro',
|
||||
// Polls
|
||||
voteOnPoll: 'Votar en encuesta',
|
||||
pollChoices: 'Opciones de la encuesta',
|
||||
vote: 'Votar',
|
||||
pollDetails: 'Detalles de la encuesta',
|
||||
refresh: 'Actualizar',
|
||||
expires: 'Finaliza',
|
||||
expired: 'Finalizada',
|
||||
voteCount: `{count, plural,
|
||||
one {1 voto}
|
||||
other {{count} votos}
|
||||
}`,
|
||||
// Status interactions
|
||||
clickToShowThread: '{time} - haz clic para mostrar el hilo',
|
||||
showMore: 'Mostrar más',
|
||||
showLess: 'Mostrar menos',
|
||||
closeReply: 'Cerrar respuesta',
|
||||
cannotReblogFollowersOnly: 'No se puede reenviar porque es solo para seguidores',
|
||||
cannotReblogDirectMessage: 'No se puede reenviar porque es un mensaje directo',
|
||||
reblog: 'Reenviar',
|
||||
reply: 'Responder',
|
||||
replyToThread: 'Responder al hilo',
|
||||
favorite: 'Favorito',
|
||||
unfavorite: 'No favorito',
|
||||
// timeline
|
||||
loadingMore: 'Cargando más…',
|
||||
loadMore: 'Cargar más',
|
||||
showCountMore: 'Mostrar {count} más',
|
||||
nothingToShow: 'Nada para mostrar.',
|
||||
// status thread page
|
||||
statusThreadPage: 'Página de hilo de toots',
|
||||
status: 'Toot',
|
||||
// toast messages
|
||||
blockedAccount: 'Cuenta bloqueada',
|
||||
unblockedAccount: 'Cuenta desbloqueada',
|
||||
unableToBlock: 'No se puede bloquear la cuenta: {error}',
|
||||
unableToUnblock: 'No se puede desbloquear la cuenta: {error}',
|
||||
bookmarkedStatus: 'Toot con marcador',
|
||||
unbookmarkedStatus: 'Toot sin marcador',
|
||||
unableToBookmark: 'No se puede poner marcador: {error}',
|
||||
unableToUnbookmark: 'No se puede quitar marcador: {error}',
|
||||
cannotPostOffline: 'No puedes publicar mientras estás sin conexión',
|
||||
unableToPost: 'No se puede publicar el toot: {error}',
|
||||
statusDeleted: 'Toot borrado',
|
||||
unableToDelete: 'No se puede borrar el toot: {error}',
|
||||
cannotFavoriteOffline: 'No puedes marcar como favorito mientras estás sin conexión',
|
||||
cannotUnfavoriteOffline: 'No puedes quitar marca de favorito mientras estás sin conexión',
|
||||
unableToFavorite: 'No se puede marcar como favorito: {error}',
|
||||
unableToUnfavorite: 'No se puede quitar marca de favorito: {error}',
|
||||
followedAccount: 'Cuenta seguida',
|
||||
unfollowedAccount: 'Cuenta no seguida',
|
||||
unableToFollow: 'No se puede seguir a la cuenta: {error}',
|
||||
unableToUnfollow: 'No se puede dejar de seguir a la cuenta: {error}',
|
||||
accessTokenRevoked: 'El token de acceso fue anulado, se cerró sesión en {instance}',
|
||||
loggedOutOfInstance: 'Se cerró sesión en {instance}',
|
||||
failedToUploadMedia: 'Falló la subida del multimedia: {error}',
|
||||
mutedAccount: 'Cuenta silenciada',
|
||||
unmutedAccount: 'Cuenta no silenciada',
|
||||
unableToMute: 'No se puede silenciar la cuenta: {error}',
|
||||
unableToUnmute: 'No se puede dejar de silenciar la cuenta: {error}',
|
||||
mutedConversation: 'Conversación silenciada',
|
||||
unmutedConversation: 'Conversación no silenciada',
|
||||
unableToMuteConversation: 'No se puede silenciar la conversación: {error}',
|
||||
unableToUnmuteConversation: 'No se puede dejar de silenciar la conversación: {error}',
|
||||
unpinnedStatus: 'Toot no fijado',
|
||||
unableToPinStatus: 'No se puede fijar el toot: {error}',
|
||||
unableToUnpinStatus: 'No se puede dejar de fijar el toot: {error}',
|
||||
unableToRefreshPoll: 'No se puede actualizar la encuesta: {error}',
|
||||
unableToVoteInPoll: 'No se puede votar en la encuesta: {error}',
|
||||
cannotReblogOffline: 'No puedes reenviar mientras estás sin conexión.',
|
||||
cannotUnreblogOffline: 'No puedes deshacer reenvíos mientras estás sin conexión.',
|
||||
failedToReblog: 'Fallo al reenviar: {error}',
|
||||
failedToUnreblog: 'Fallo al deshacer reenvío: {error}',
|
||||
submittedReport: 'Denuncia enviada',
|
||||
failedToReport: 'Fallo al enviar denuncia: {error}',
|
||||
approvedFollowRequest: 'Solicitud de seguimiento aceptada',
|
||||
rejectedFollowRequest: 'Solicitud de seguimiento rechazada',
|
||||
unableToApproveFollowRequest: 'No se puede aceptar la solicitud de seguimiento: {error}',
|
||||
unableToRejectFollowRequest: 'No se puede rechazar la solicitud de seguimiento: {error}',
|
||||
searchError: 'Error durante la búsqueda: {error}',
|
||||
hidDomain: 'Dominio oculto',
|
||||
unhidDomain: 'Dominio no oculto',
|
||||
unableToHideDomain: 'No se puede ocultar el dominio: {error}',
|
||||
unableToUnhideDomain: 'No se puede dejar de ocultar el dominio: {error}',
|
||||
showingReblogs: 'Mostrando reenvíos',
|
||||
hidingReblogs: 'Ocultando reenvíos',
|
||||
unableToShowReblogs: 'No se puede mostrar los reenvíos: {error}',
|
||||
unableToHideReblogs: 'No se puede ocultar los reenvíos: {error}',
|
||||
unableToShare: 'No se puede compartir: {error}',
|
||||
unableToSubscribe: 'Imposible suscribirse: {error}',
|
||||
unableToUnsubscribe: 'Imposible dejar de suscribirse: {error}',
|
||||
showingOfflineContent: 'La petición a internet falló. Mostrando contenido sin conexión.',
|
||||
youAreOffline: 'Parece que estás sin conexión. Puedes leer contenido incluso sin conexión.',
|
||||
// Snackbar UI
|
||||
updateAvailable: 'Actualización de la aplicación disponible.',
|
||||
// Word/phrase filters
|
||||
wordFilters: 'Filtros de palabras',
|
||||
noFilters: 'No tienes ningún filtro de palabras.',
|
||||
wordOrPhrase: 'Palabra o frase',
|
||||
contexts: 'Contextos',
|
||||
addFilter: 'Añadir filtro',
|
||||
editFilter: 'Editar filtro',
|
||||
filterHome: 'Inicio y listas',
|
||||
filterNotifications: 'Notificaciones',
|
||||
filterPublic: 'Cronologías públicas',
|
||||
filterThread: 'Conversaciones',
|
||||
filterAccount: 'Perfiles',
|
||||
filterUnknown: 'Desconocido',
|
||||
expireAfter: 'Expira al cabo de',
|
||||
whereToFilter: 'Dónde filtrar',
|
||||
irreversible: 'Irreversible',
|
||||
wholeWord: 'Palabra completa',
|
||||
save: 'Guardar',
|
||||
updatedFilter: 'Filtro actualizado',
|
||||
createdFilter: 'Filtro creado',
|
||||
failedToModifyFilter: 'Fallo al modificar el filtro: {error}',
|
||||
deletedFilter: 'Filtro borrado',
|
||||
required: 'Requerido',
|
||||
// Dialogs
|
||||
profileOptions: 'Opciones de perfil',
|
||||
copyLink: 'Copiar enlace',
|
||||
emoji: 'Emoji',
|
||||
editMedia: 'Editar multimedia',
|
||||
shortcutHelp: 'Ayuda sobre atajos de teclado',
|
||||
statusOptions: 'Opciones de estado',
|
||||
confirm: 'Confirmar',
|
||||
closeDialog: 'Cerrar diálogo',
|
||||
postPrivacy: 'Privacidad del post',
|
||||
homeOnInstance: 'Inicio en {instance}',
|
||||
statusesTimelineOnInstance: 'Estados: {timeline} cronología en {instance}',
|
||||
statusesHashtag: 'Estados: #{hashtag} hashtag',
|
||||
statusesThread: 'Estados: hilo',
|
||||
statusesAccountTimeline: 'Estado: cronología de cuenta',
|
||||
statusesList: 'Estado: lista',
|
||||
notificationsOnInstance: 'Notificaciones en {instance}'
|
||||
}
|
|
@ -189,8 +189,6 @@ export default {
|
|||
}`,
|
||||
pinPage: 'Epingler {label}',
|
||||
// Status composition
|
||||
overLimit: '{count} {count, plural, =1 {caractère} other {caractères}} en dessus de la limite',
|
||||
underLimit: '{count} {count, plural, =1 {caractère} other {caractères}} qui reste',
|
||||
composeStatus: 'Ecrire un pouet',
|
||||
postStatus: 'Pouet!',
|
||||
contentWarning: 'Avertissement',
|
||||
|
|
|
@ -17,7 +17,7 @@ export default {
|
|||
logIn: 'Войти',
|
||||
footer: `
|
||||
<p>
|
||||
Pinafore — это
|
||||
Pinafore — это
|
||||
<a rel="noopener" target="_blank" href="https://github.com/nolanlawson/pinafore">программное обеспечение с открытым исходным кодом</a>
|
||||
созданное
|
||||
<a rel="noopener" target="_blank" href="https://nolanlawson.com">Ноланом Лоусоном</a>
|
||||
|
@ -192,8 +192,6 @@ export default {
|
|||
}`,
|
||||
pinPage: 'Закрепить {label}',
|
||||
// Status composition
|
||||
overLimit: '{count} {count, plural, =1 {символ} other {символов}} сверх лимита',
|
||||
underLimit: '{count} {count, plural, =1 {символ} other {символов}} осталось',
|
||||
composeStatus: 'Создать запись',
|
||||
postStatus: 'Опубликовать!',
|
||||
contentWarning: 'Предупреждение о содержимом',
|
||||
|
|
|
@ -11,6 +11,8 @@ function getNotificationText (notification, omitEmojiInDisplayNames) {
|
|||
return formatIntl('intl.accountRebloggedYou', { account: notificationAccountDisplayName })
|
||||
} else if (notification.type === 'favourite') {
|
||||
return formatIntl('intl.accountFavoritedYou', { account: notificationAccountDisplayName })
|
||||
} else if (notification.type === 'update') {
|
||||
return formatIntl('intl.accountEdited', { account: notificationAccountDisplayName })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,12 +39,15 @@ function cleanupText (text) {
|
|||
export function getAccessibleLabelForStatus (originalAccount, account, plainTextContent,
|
||||
shortInlineFormattedDate, spoilerText, showContent,
|
||||
reblog, notification, visibility, omitEmojiInDisplayNames,
|
||||
disableLongAriaLabels, showMedia, showPoll) {
|
||||
disableLongAriaLabels, showMedia, sensitive, sensitiveShown, mediaAttachments, showPoll) {
|
||||
const originalAccountDisplayName = getAccountAccessibleName(originalAccount, omitEmojiInDisplayNames)
|
||||
const contentTextToShow = (showContent || !spoilerText)
|
||||
? cleanupText(plainTextContent)
|
||||
: formatIntl('intl.contentWarningContent', { spoiler: cleanupText(spoilerText) })
|
||||
const mediaTextToShow = showMedia && 'intl.hasMedia'
|
||||
const mediaDescText = (showMedia && (!sensitive || sensitiveShown))
|
||||
? mediaAttachments.map(media => media.description)
|
||||
: []
|
||||
const pollTextToShow = showPoll && 'intl.hasPoll'
|
||||
const privacyText = getPrivacyText(visibility)
|
||||
|
||||
|
@ -57,6 +62,7 @@ export function getAccessibleLabelForStatus (originalAccount, account, plainText
|
|||
originalAccountDisplayName,
|
||||
contentTextToShow,
|
||||
mediaTextToShow,
|
||||
...mediaDescText,
|
||||
pollTextToShow,
|
||||
shortInlineFormattedDate,
|
||||
`@${originalAccount.acct}`,
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
import { database } from '../_database/database.js'
|
||||
import { decode as decodeBlurhash, init as initBlurhash } from '../_utils/blurhash.js'
|
||||
import { mark, stop } from '../_utils/marks.js'
|
||||
import { get } from '../_utils/lodash-lite.js'
|
||||
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText.js'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
|
||||
import { prepareToRehydrate, rehydrateStatusOrNotification } from './rehydrateStatusOrNotification.js'
|
||||
|
||||
async function getNotification (instanceName, timelineType, timelineValue, itemId) {
|
||||
return {
|
||||
|
@ -21,62 +18,10 @@ async function getStatus (instanceName, timelineType, timelineValue, itemId) {
|
|||
}
|
||||
}
|
||||
|
||||
function tryInitBlurhash () {
|
||||
try {
|
||||
initBlurhash()
|
||||
} catch (err) {
|
||||
console.error('could not start blurhash worker', err)
|
||||
}
|
||||
}
|
||||
|
||||
function getActualStatus (statusOrNotification) {
|
||||
return get(statusOrNotification, ['status']) ||
|
||||
get(statusOrNotification, ['notification', 'status'])
|
||||
}
|
||||
|
||||
async function decodeAllBlurhashes (statusOrNotification) {
|
||||
const status = getActualStatus(statusOrNotification)
|
||||
if (!status) {
|
||||
return
|
||||
}
|
||||
const mediaWithBlurhashes = get(status, ['media_attachments'], [])
|
||||
.concat(get(status, ['reblog', 'media_attachments'], []))
|
||||
.filter(_ => _.blurhash)
|
||||
if (mediaWithBlurhashes.length) {
|
||||
mark(`decodeBlurhash-${status.id}`)
|
||||
await Promise.all(mediaWithBlurhashes.map(async media => {
|
||||
try {
|
||||
media.decodedBlurhash = await decodeBlurhash(media.blurhash)
|
||||
} catch (err) {
|
||||
console.warn('Could not decode blurhash, ignoring', err)
|
||||
}
|
||||
}))
|
||||
stop(`decodeBlurhash-${status.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function calculatePlainTextContent (statusOrNotification) {
|
||||
const status = getActualStatus(statusOrNotification)
|
||||
if (!status) {
|
||||
return
|
||||
}
|
||||
const originalStatus = status.reblog ? status.reblog : status
|
||||
const content = originalStatus.content || ''
|
||||
const mentions = originalStatus.mentions || []
|
||||
// Calculating the plaintext from the HTML is a non-trivial operation, so we might
|
||||
// as well do it in advance, while blurhash is being decoded on the worker thread.
|
||||
await new Promise(resolve => {
|
||||
scheduleIdleTask(() => {
|
||||
originalStatus.plainTextContent = statusHtmlToPlainText(content, mentions)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function createMakeProps (instanceName, timelineType, timelineValue) {
|
||||
let promiseChain = Promise.resolve()
|
||||
|
||||
tryInitBlurhash() // start the blurhash worker a bit early to save time
|
||||
prepareToRehydrate() // start blurhash early to save time
|
||||
|
||||
async function fetchFromIndexedDB (itemId) {
|
||||
mark(`fetchFromIndexedDB-${itemId}`)
|
||||
|
@ -92,10 +37,7 @@ export function createMakeProps (instanceName, timelineType, timelineValue) {
|
|||
|
||||
async function getStatusOrNotification (itemId) {
|
||||
const statusOrNotification = await fetchFromIndexedDB(itemId)
|
||||
await Promise.all([
|
||||
decodeAllBlurhashes(statusOrNotification),
|
||||
calculatePlainTextContent(statusOrNotification)
|
||||
])
|
||||
await rehydrateStatusOrNotification(statusOrNotification)
|
||||
return statusOrNotification
|
||||
}
|
||||
|
||||
|
|
|
@ -4,18 +4,28 @@ import { database } from '../_database/database.js'
|
|||
import {
|
||||
getPinnedStatuses
|
||||
} from '../_api/pinnedStatuses.js'
|
||||
import { prepareToRehydrate, rehydrateStatusOrNotification } from './rehydrateStatusOrNotification.js'
|
||||
|
||||
// Pinned statuses aren't a "normal" timeline, so their blurhashes/plaintext need to be calculated specially
|
||||
async function rehydratePinnedStatuses (statuses) {
|
||||
await Promise.all(statuses.map(status => rehydrateStatusOrNotification({ status })))
|
||||
return statuses
|
||||
}
|
||||
|
||||
export async function updatePinnedStatusesForAccount (accountId) {
|
||||
const { currentInstance, accessToken } = store.get()
|
||||
|
||||
await cacheFirstUpdateAfter(
|
||||
() => getPinnedStatuses(currentInstance, accessToken, accountId),
|
||||
async () => {
|
||||
return rehydratePinnedStatuses(await getPinnedStatuses(currentInstance, accessToken, accountId))
|
||||
},
|
||||
async () => {
|
||||
prepareToRehydrate() // start blurhash early to save time
|
||||
const pinnedStatuses = await database.getPinnedStatuses(currentInstance, accountId)
|
||||
if (!pinnedStatuses || !pinnedStatuses.every(Boolean)) {
|
||||
throw new Error('missing pinned statuses in idb')
|
||||
}
|
||||
return pinnedStatuses
|
||||
return rehydratePinnedStatuses(pinnedStatuses)
|
||||
},
|
||||
statuses => database.insertPinnedStatuses(currentInstance, accountId, statuses),
|
||||
statuses => {
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { get } from '../_utils/lodash-lite.js'
|
||||
import { mark, stop } from '../_utils/marks.js'
|
||||
import { decode as decodeBlurhash, init as initBlurhash } from '../_utils/blurhash.js'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
|
||||
import { statusHtmlToPlainText } from '../_utils/statusHtmlToPlainText.js'
|
||||
|
||||
function getActualStatus (statusOrNotification) {
|
||||
return get(statusOrNotification, ['status']) ||
|
||||
get(statusOrNotification, ['notification', 'status'])
|
||||
}
|
||||
|
||||
export function prepareToRehydrate () {
|
||||
// start the blurhash worker a bit early to save time
|
||||
try {
|
||||
initBlurhash()
|
||||
} catch (err) {
|
||||
console.error('could not start blurhash worker', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function decodeAllBlurhashes (statusOrNotification) {
|
||||
const status = getActualStatus(statusOrNotification)
|
||||
if (!status) {
|
||||
return
|
||||
}
|
||||
const mediaWithBlurhashes = get(status, ['media_attachments'], [])
|
||||
.concat(get(status, ['reblog', 'media_attachments'], []))
|
||||
.filter(_ => _.blurhash)
|
||||
if (mediaWithBlurhashes.length) {
|
||||
mark(`decodeBlurhash-${status.id}`)
|
||||
await Promise.all(mediaWithBlurhashes.map(async media => {
|
||||
try {
|
||||
media.decodedBlurhash = await decodeBlurhash(media.blurhash)
|
||||
} catch (err) {
|
||||
console.warn('Could not decode blurhash, ignoring', err)
|
||||
}
|
||||
}))
|
||||
stop(`decodeBlurhash-${status.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function calculatePlainTextContent (statusOrNotification) {
|
||||
const status = getActualStatus(statusOrNotification)
|
||||
if (!status) {
|
||||
return
|
||||
}
|
||||
const originalStatus = status.reblog ? status.reblog : status
|
||||
const content = originalStatus.content || ''
|
||||
const mentions = originalStatus.mentions || []
|
||||
// Calculating the plaintext from the HTML is a non-trivial operation, so we might
|
||||
// as well do it in advance, while blurhash is being decoded on the worker thread.
|
||||
await new Promise(resolve => {
|
||||
scheduleIdleTask(() => {
|
||||
originalStatus.plainTextContent = statusHtmlToPlainText(content, mentions)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Do stuff that we need to do when the status or notification is fetched from the database,
|
||||
// like calculating the blurhash or calculating the plain text content
|
||||
export async function rehydrateStatusOrNotification (statusOrNotification) {
|
||||
await Promise.all([
|
||||
decodeAllBlurhashes(statusOrNotification),
|
||||
calculatePlainTextContent(statusOrNotification)
|
||||
])
|
||||
}
|
|
@ -2,8 +2,9 @@ import { mark, stop } from '../../_utils/marks.js'
|
|||
import { deleteStatus } from '../deleteStatuses.js'
|
||||
import { addStatusOrNotification } from '../addStatusOrNotification.js'
|
||||
import { emit } from '../../_utils/eventBus.js'
|
||||
import { updateStatus } from '../updateStatus.js'
|
||||
|
||||
const KNOWN_EVENTS = ['update', 'delete', 'notification', 'conversation', 'filters_changed']
|
||||
const KNOWN_EVENTS = ['update', 'delete', 'notification', 'conversation', 'filters_changed', 'status.update']
|
||||
|
||||
export function processMessage (instanceName, timelineName, message) {
|
||||
let { event, payload } = (message || {})
|
||||
|
@ -12,7 +13,7 @@ export function processMessage (instanceName, timelineName, message) {
|
|||
return
|
||||
}
|
||||
mark('processMessage')
|
||||
if (['update', 'notification', 'conversation'].includes(event)) {
|
||||
if (['update', 'notification', 'conversation', 'status.update'].includes(event)) {
|
||||
payload = JSON.parse(payload) // only these payloads are JSON-encoded for some reason
|
||||
}
|
||||
|
||||
|
@ -43,6 +44,9 @@ export function processMessage (instanceName, timelineName, message) {
|
|||
case 'filters_changed':
|
||||
emit('wordFiltersChanged', instanceName)
|
||||
break
|
||||
case 'status.update':
|
||||
updateStatus(instanceName, payload)
|
||||
break
|
||||
}
|
||||
stop('processMessage')
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { database } from '../_database/database.js'
|
||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask.js'
|
||||
|
||||
async function doUpdateStatus (instanceName, newStatus) {
|
||||
console.log('updating status', newStatus)
|
||||
await database.updateStatus(instanceName, newStatus)
|
||||
}
|
||||
|
||||
export function updateStatus (instanceName, newStatus) {
|
||||
scheduleIdleTask(() => {
|
||||
/* no await */ doUpdateStatus(instanceName, newStatus)
|
||||
})
|
||||
}
|
|
@ -27,11 +27,13 @@ export function generateAuthLink (instanceName, clientId, redirectUri) {
|
|||
|
||||
export function getAccessTokenFromAuthCode (instanceName, clientId, clientSecret, code, redirectUri) {
|
||||
const url = `${basename(instanceName)}/oauth/token`
|
||||
return post(url, {
|
||||
// Using URLSearchParams here guarantees a content type of application/x-www-form-urlencoded
|
||||
// See https://fetch.spec.whatwg.org/#bodyinit-unions
|
||||
return post(url, new URLSearchParams({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret,
|
||||
redirect_uri: redirectUri,
|
||||
grant_type: 'authorization_code',
|
||||
code
|
||||
}, null, { timeout: WRITE_TIMEOUT })
|
||||
}), null, { timeout: WRITE_TIMEOUT })
|
||||
}
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
import { auth, basename } from './utils.js'
|
||||
import { DEFAULT_TIMEOUT, get, post, WRITE_TIMEOUT } from '../_utils/ajax.js'
|
||||
import { DEFAULT_TIMEOUT, get, post, put, WRITE_TIMEOUT } from '../_utils/ajax.js'
|
||||
|
||||
export async function postStatus (instanceName, accessToken, text, inReplyToId, mediaIds,
|
||||
// post is create, put is edit
|
||||
async function postOrPutStatus (url, accessToken, method, text, inReplyToId, mediaIds,
|
||||
sensitive, spoilerText, visibility, poll) {
|
||||
const url = `${basename(instanceName)}/api/v1/statuses`
|
||||
|
||||
const body = {
|
||||
status: text,
|
||||
in_reply_to_id: inReplyToId,
|
||||
media_ids: mediaIds,
|
||||
sensitive,
|
||||
spoiler_text: spoilerText,
|
||||
visibility,
|
||||
poll
|
||||
poll,
|
||||
...(method === 'post' && {
|
||||
// you can't change these properties when editing
|
||||
in_reply_to_id: inReplyToId,
|
||||
visibility
|
||||
})
|
||||
}
|
||||
|
||||
for (const key of Object.keys(body)) {
|
||||
|
@ -23,7 +25,23 @@ export async function postStatus (instanceName, accessToken, text, inReplyToId,
|
|||
}
|
||||
}
|
||||
|
||||
return post(url, body, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
||||
const func = method === 'post' ? post : put
|
||||
|
||||
return func(url, body, auth(accessToken), { timeout: WRITE_TIMEOUT })
|
||||
}
|
||||
|
||||
export async function postStatus (instanceName, accessToken, text, inReplyToId, mediaIds,
|
||||
sensitive, spoilerText, visibility, poll) {
|
||||
const url = `${basename(instanceName)}/api/v1/statuses`
|
||||
return postOrPutStatus(url, accessToken, 'post', text, inReplyToId, mediaIds,
|
||||
sensitive, spoilerText, visibility, poll)
|
||||
}
|
||||
|
||||
export async function putStatus (instanceName, accessToken, id, text, inReplyToId, mediaIds,
|
||||
sensitive, spoilerText, visibility, poll) {
|
||||
const url = `${basename(instanceName)}/api/v1/statuses/${id}`
|
||||
return postOrPutStatus(url, accessToken, 'put', text, inReplyToId, mediaIds,
|
||||
sensitive, spoilerText, visibility, poll)
|
||||
}
|
||||
|
||||
export async function getStatusContext (instanceName, accessToken, statusId) {
|
||||
|
|
|
@ -66,7 +66,7 @@ export async function getTimeline (instanceName, accessToken, timeline, maxId, s
|
|||
}
|
||||
|
||||
if (timeline === 'notifications/mentions') {
|
||||
params.exclude_types = ['follow', 'favourite', 'reblog', 'poll', 'admin.sign_up']
|
||||
params.exclude_types = ['follow', 'favourite', 'reblog', 'poll', 'admin.sign_up', 'update', 'follow_request', 'admin.report']
|
||||
}
|
||||
|
||||
url += '?' + paramsString(params)
|
||||
|
|
|
@ -8,7 +8,10 @@
|
|||
<button type="button"
|
||||
class="dynamic-page-go-back"
|
||||
aria-label="{intl.goBack}"
|
||||
on:click|preventDefault="onGoBack()">{intl.back}</button>
|
||||
on:click|preventDefault="onGoBack()">
|
||||
<SvgIcon className="dynamic-page-go-back-icon" href="#fa-arrow-left" />
|
||||
{intl.back}
|
||||
</button>
|
||||
</div>
|
||||
<Shortcut key="Backspace" on:pressed="onGoBack()"/>
|
||||
<style>
|
||||
|
@ -34,19 +37,25 @@
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
.dynamic-page-go-back {
|
||||
font-size: 1.3em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-self: flex-end;
|
||||
font-size: 1.2857142857142858em;
|
||||
color: var(--anchor-text);
|
||||
border: 0;
|
||||
padding: 0;
|
||||
background: none;
|
||||
justify-self: flex-end;
|
||||
}
|
||||
.dynamic-page-go-back:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.dynamic-page-go-back::before {
|
||||
content: '←';
|
||||
margin-right: 5px;
|
||||
:global(.dynamic-page-go-back-icon) {
|
||||
position: relative;
|
||||
bottom: 0.06em;
|
||||
margin-right: 0.2em;
|
||||
height: 0.66666666em;
|
||||
width: 0.66666666em;
|
||||
fill: currentColor;
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.dynamic-page-banner {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<span class="length-indicator {overLimit ? 'over-char-limit' : ''}"
|
||||
aria-label={lengthLabel}
|
||||
aria-live={lengthVerbosity}
|
||||
aria-atomic='true'
|
||||
{style}
|
||||
>{lengthToDisplayDeferred}</span>
|
||||
<style>
|
||||
|
@ -17,10 +18,11 @@
|
|||
import { store } from '../_store/store.js'
|
||||
import { observe } from 'svelte-extras'
|
||||
import { throttleTimer } from '../_utils/throttleTimer.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
|
||||
const updateDisplayedLength = process.browser && throttleTimer(requestAnimationFrame)
|
||||
|
||||
// How many chars within the limit to start warning at
|
||||
const WARN_THRESHOLD = 10
|
||||
|
||||
export default {
|
||||
oncreate () {
|
||||
const { lengthToDisplay } = this.get()
|
||||
|
@ -42,11 +44,12 @@
|
|||
store: () => store,
|
||||
computed: {
|
||||
lengthToDisplay: ({ length, max }) => max - length,
|
||||
lengthLabel: ({ overLimit, lengthToDisplayDeferred }) => {
|
||||
if (overLimit) {
|
||||
return formatIntl('intl.overLimit', { count: -lengthToDisplayDeferred })
|
||||
lengthVerbosity: ({ lengthToDisplayDeferred }) => {
|
||||
// When approaching the limit, notify screen reader users
|
||||
if (lengthToDisplayDeferred > WARN_THRESHOLD) {
|
||||
return 'off'
|
||||
} else {
|
||||
return formatIntl('intl.underLimit', { count: lengthToDisplayDeferred })
|
||||
return 'polite'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
/>
|
||||
<span class="nav-link-label">{label}</span>
|
||||
</div>
|
||||
<div class="nav-indicator-wrapper">
|
||||
<div class="nav-indicator-wrapper {animationClasses}">
|
||||
<div class="nav-indicator" ref:indicator></div>
|
||||
</div>
|
||||
</a>
|
||||
|
@ -45,35 +45,36 @@
|
|||
.nav-indicator-wrapper {
|
||||
width: 100%;
|
||||
height: var(--nav-indicator-height);
|
||||
background: var(--nav-a-border);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav-indicator {
|
||||
flex: 1;
|
||||
background: var(--nav-a-border);
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
.nav-indicator.animate {
|
||||
.nav-indicator {
|
||||
background: var(--nav-indicator-bg);
|
||||
}
|
||||
|
||||
.nav-indicator-wrapper.animating > .nav-indicator {
|
||||
transition: transform 333ms ease-in-out;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.main-nav-link:hover .nav-indicator {
|
||||
background: var(--nav-a-border-hover);
|
||||
}
|
||||
|
||||
.main-nav-link.selected .nav-indicator-wrapper {
|
||||
background: var(--nav-a-border-hover);
|
||||
background: var(--nav-indicator-bg-hover);
|
||||
}
|
||||
|
||||
.main-nav-link.selected .nav-indicator {
|
||||
background: var(--nav-indicator-bg);
|
||||
background: var(--nav-indicator-bg-active);
|
||||
}
|
||||
|
||||
.main-nav-link.selected:hover .nav-indicator {
|
||||
background: var(--nav-indicator-bg-hover);
|
||||
/* Desktop/mouse only https://medium.com/@mezoistvan/finally-a-css-only-solution-to-hover-on-touchscreens-c498af39c31c */
|
||||
@media(hover: hover) and (pointer: fine) {
|
||||
.main-nav-link:hover .nav-indicator-wrapper.pre-animating {
|
||||
background: var(--nav-indicator-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.main-nav-link:hover {
|
||||
|
@ -129,6 +130,7 @@
|
|||
import { scrollToTop } from '../_utils/scrollToTop.js'
|
||||
import { normalizePageName } from '../_utils/normalizePageName.js'
|
||||
import { formatIntl } from '../_utils/formatIntl.js'
|
||||
import { classname } from '../_utils/classname.js'
|
||||
|
||||
export default {
|
||||
oncreate () {
|
||||
|
@ -148,6 +150,10 @@
|
|||
})
|
||||
},
|
||||
store: () => store,
|
||||
data: () => ({
|
||||
preAnimating: false,
|
||||
animating: false
|
||||
}),
|
||||
computed: {
|
||||
selected: ({ page, name }) => name === normalizePageName(page),
|
||||
ariaLabel: ({ selected, name, label, $numberOfNotifications, $numberOfFollowRequests }) => {
|
||||
|
@ -166,6 +172,10 @@
|
|||
),
|
||||
badgeNumber: ({ name, $numberOfNotifications, $numberOfFollowRequests }) => (
|
||||
(name === 'notifications' && $numberOfNotifications) || (name === 'community' && $numberOfFollowRequests) || 0
|
||||
),
|
||||
animationClasses: ({ animating, preAnimating }) => classname(
|
||||
animating && 'animating',
|
||||
preAnimating && 'pre-animating'
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
|
@ -187,7 +197,7 @@
|
|||
emit('animateNavPart2', { fromRect, toPage })
|
||||
},
|
||||
animatePart2 ({ fromRect }) {
|
||||
const indicator = this.refs.indicator
|
||||
const { indicator } = this.refs
|
||||
mark('animateNavPart2 gBCR')
|
||||
const toRect = indicator.getBoundingClientRect()
|
||||
stop('animateNavPart2 gBCR')
|
||||
|
@ -196,11 +206,12 @@
|
|||
indicator.style.transform = `translateX(${translateX}px) scaleX(${scaleX})`
|
||||
const onTransitionEnd = () => {
|
||||
indicator.removeEventListener('transitionend', onTransitionEnd)
|
||||
indicator.classList.remove('animate')
|
||||
this.set({ animating: false, preAnimating: false })
|
||||
}
|
||||
indicator.addEventListener('transitionend', onTransitionEnd)
|
||||
this.set({ preAnimating: true }) // avoids a flicker before the doubleRAF
|
||||
doubleRAF(() => {
|
||||
indicator.classList.add('animate')
|
||||
this.set({ animating: true })
|
||||
indicator.style.transform = ''
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
-->
|
||||
<span class="tooltip-button"
|
||||
aria-describedby="tooltip-{id}"
|
||||
aria-expanded={shown}
|
||||
aria-controls="tooltip-{id}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:mouseover="set({shown: true, mouseover: true})"
|
||||
|
|
|
@ -76,6 +76,10 @@
|
|||
}
|
||||
if (notificationType === 'admin.sign_up') {
|
||||
return formatIntl('intl.accountSignedUp', params)
|
||||
} else if (notificationType === 'follow_request') {
|
||||
return formatIntl('intl.accountRequestedFollow', params)
|
||||
} else if (notificationType === 'admin.report') {
|
||||
return formatIntl('intl.accountReported', params)
|
||||
} else { // 'follow'
|
||||
return formatIntl('intl.accountFollowedYou', params)
|
||||
}
|
||||
|
|
|
@ -39,7 +39,9 @@
|
|||
{#if isStatusInOwnThread}
|
||||
<StatusDetails {...params} {...timestampParams} />
|
||||
{/if}
|
||||
<StatusToolbar {...params} {replyShown} on:recalculateHeight />
|
||||
{#if !isStatusInNotification}
|
||||
<StatusToolbar {...params} {replyShown} on:recalculateHeight on:focusArticle="focusArticle()" />
|
||||
{/if}
|
||||
{#if replyShown}
|
||||
<StatusComposeBox {...params} on:recalculateHeight />
|
||||
{/if}
|
||||
|
@ -133,6 +135,7 @@
|
|||
import { composeNewStatusMentioning } from '../../_actions/mention.js'
|
||||
import { createStatusOrNotificationUuid } from '../../_utils/createStatusOrNotificationUuid.js'
|
||||
import { addEmojiTooltips } from '../../_utils/addEmojiTooltips.js'
|
||||
import { tryToFocusElement } from '../../_utils/tryToFocusElement.js'
|
||||
|
||||
const INPUT_TAGS = new Set(['a', 'button', 'input', 'textarea', 'label'])
|
||||
const isUserInputElement = node => INPUT_TAGS.has(node.localName)
|
||||
|
@ -213,6 +216,10 @@
|
|||
async mentionAuthor () {
|
||||
const { accountForShortcut } = this.get()
|
||||
await composeNewStatusMentioning(accountForShortcut)
|
||||
},
|
||||
focusArticle () {
|
||||
const { elementId } = this.get()
|
||||
tryToFocusElement(elementId, /* scroll */ true)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -253,7 +260,7 @@
|
|||
notification && notification.status &&
|
||||
notification.type !== 'mention' && notification.status.id === originalStatusId
|
||||
),
|
||||
spoilerShown: ({ $spoilersShown, uuid }) => !!$spoilersShown[uuid],
|
||||
spoilerShown: ({ $spoilersShown, uuid, $showAllSpoilers }) => (typeof $spoilersShown[uuid] === 'undefined' ? !!$showAllSpoilers : !!$spoilersShown[uuid]),
|
||||
replyShown: ({ $repliesShown, uuid }) => !!$repliesShown[uuid],
|
||||
showCard: ({ originalStatus, isStatusInNotification, showMedia, $hideCards }) => (
|
||||
!$hideCards &&
|
||||
|
@ -270,6 +277,13 @@
|
|||
originalStatus.media_attachments &&
|
||||
originalStatus.media_attachments.length
|
||||
),
|
||||
mediaAttachments: ({ originalStatus }) => (
|
||||
originalStatus.media_attachments
|
||||
),
|
||||
sensitiveShown: ({ $sensitivesShown, uuid }) => !!$sensitivesShown[uuid],
|
||||
sensitive: ({ originalStatus, $markMediaAsSensitive, $neverMarkMediaAsSensitive }) => (
|
||||
!$neverMarkMediaAsSensitive && ($markMediaAsSensitive || originalStatus.sensitive)
|
||||
),
|
||||
originalAccountEmojis: ({ originalAccount }) => (originalAccount.emojis || []),
|
||||
originalStatusEmojis: ({ originalStatus }) => (originalStatus.emojis || []),
|
||||
originalAccountDisplayName: ({ originalAccount }) => (originalAccount.display_name || originalAccount.username),
|
||||
|
@ -288,16 +302,16 @@
|
|||
ariaLabel: ({
|
||||
originalAccount, account, plainTextContent, shortInlineFormattedDate, spoilerText,
|
||||
showContent, reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels,
|
||||
showMedia, showPoll
|
||||
showMedia, sensitive, sensitiveShown, mediaAttachments, showPoll
|
||||
}) => (
|
||||
getAccessibleLabelForStatus(originalAccount, account, plainTextContent,
|
||||
shortInlineFormattedDate, spoilerText, showContent,
|
||||
reblog, notification, visibility, $omitEmojiInDisplayNames, $disableLongAriaLabels,
|
||||
showMedia, showPoll
|
||||
showMedia, sensitive, sensitiveShown, mediaAttachments, showPoll
|
||||
)
|
||||
),
|
||||
showHeader: ({ notification, status, timelineType }) => (
|
||||
(notification && ['reblog', 'favourite', 'poll', 'status'].includes(notification.type)) ||
|
||||
(notification && ['reblog', 'favourite', 'poll', 'status', 'update'].includes(notification.type)) ||
|
||||
status.reblog ||
|
||||
timelineType === 'pinned'
|
||||
),
|
||||
|
|
|
@ -137,6 +137,12 @@
|
|||
return '#fa-comment'
|
||||
} else if (notificationType === 'admin.sign_up') {
|
||||
return '#fa-user-plus'
|
||||
} else if (notificationType === 'update') {
|
||||
return '#fa-pencil'
|
||||
} else if (notificationType === 'follow_request') {
|
||||
return '#fa-hourglass'
|
||||
} else if (notificationType === 'admin.report') {
|
||||
return '#fa-flag'
|
||||
}
|
||||
return '#fa-star'
|
||||
},
|
||||
|
@ -159,6 +165,12 @@
|
|||
}
|
||||
} else if (status && status.reblog) {
|
||||
return 'intl.reblogged'
|
||||
} else if (notificationType === 'update') {
|
||||
return 'intl.edited'
|
||||
} else if (notificationType === 'follow_request') {
|
||||
return 'intl.requestedFollow'
|
||||
} else if (notificationType === 'admin.report') {
|
||||
return 'intl.reported'
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
|
|
|
@ -122,7 +122,7 @@
|
|||
}
|
||||
|
||||
.status-in-notification svg {
|
||||
opacity: 0.5;
|
||||
stroke: var(--very-deemphasized-text-color);
|
||||
}
|
||||
|
||||
.status-in-own-thread .option-text {
|
||||
|
@ -307,7 +307,10 @@
|
|||
expired: ({ poll }) => poll.expired,
|
||||
expiresAt: ({ poll }) => poll.expires_at,
|
||||
// Misskey can have polls that never end. These give expiresAt as null
|
||||
expiresAtTS: ({ expiresAt }) => typeof expiresAt === 'number' ? new Date(expiresAt).getTime() : null,
|
||||
// Also, Mastodon v4+ uses ISO strings, whereas Mastodon pre-v4 used numbers
|
||||
expiresAtTS: ({ expiresAt }) => (
|
||||
(typeof expiresAt === 'number' || typeof expiresAt === 'string') ? new Date(expiresAt).getTime() : null
|
||||
),
|
||||
expiresAtTimeagoFormatted: ({ expiresAtTS, expired, $now }) => (
|
||||
expired ? formatTimeagoDate(expiresAtTS, $now) : formatTimeagoFutureDate(expiresAtTS, $now)
|
||||
),
|
||||
|
|
|
@ -76,8 +76,9 @@
|
|||
methods: {
|
||||
toggleSpoilers (shown) {
|
||||
const { uuid } = this.get()
|
||||
const { spoilersShown } = this.store.get()
|
||||
spoilersShown[uuid] = typeof shown === 'undefined' ? !spoilersShown[uuid] : !!shown
|
||||
const { spoilersShown, showAllSpoilers } = this.store.get()
|
||||
const currentValue = typeof spoilersShown[uuid] === 'undefined' ? !!showAllSpoilers : spoilersShown[uuid]
|
||||
spoilersShown[uuid] = typeof shown === 'undefined' ? !currentValue : !!shown
|
||||
this.store.set({ spoilersShown })
|
||||
requestAnimationFrame(() => {
|
||||
mark('clickSpoilerButton')
|
||||
|
|
|
@ -42,7 +42,9 @@
|
|||
{#if enableShortcuts}
|
||||
<Shortcut scope={shortcutScope} key="f" on:pressed="toggleFavorite(true)"/>
|
||||
<Shortcut scope={shortcutScope} key="r" on:pressed="reply()"/>
|
||||
<Shortcut scope={shortcutScope} key="escape" on:pressed="dismiss()"/>
|
||||
<Shortcut scope={shortcutScope} key="b" on:pressed="reblog(true)"/>
|
||||
<Shortcut scope={shortcutScope} key="a" on:pressed="bookmark()"/>
|
||||
{/if}
|
||||
<style>
|
||||
.status-toolbar {
|
||||
|
@ -80,6 +82,7 @@
|
|||
import { CHECKMARK_ANIMATION, FAVORITE_ANIMATION, REBLOG_ANIMATION } from '../../_static/animations.js'
|
||||
import { on } from '../../_utils/eventBus.js'
|
||||
import { announceAriaLivePolite } from '../../_utils/announceAriaLivePolite.js'
|
||||
import { setStatusBookmarkedOrUnbookmarked } from '../../_actions/bookmark.js'
|
||||
|
||||
export default {
|
||||
oncreate () {
|
||||
|
@ -146,6 +149,13 @@
|
|||
this.fire('recalculateHeight')
|
||||
})
|
||||
},
|
||||
dismiss () {
|
||||
const { replyShown } = this.get()
|
||||
if (replyShown) {
|
||||
this.reply()
|
||||
this.fire('focusArticle')
|
||||
}
|
||||
},
|
||||
async onOptionsClick () {
|
||||
const { originalStatus, originalAccountId } = this.get()
|
||||
const updateRelationshipPromise = updateProfileAndRelationship(originalAccountId)
|
||||
|
@ -166,6 +176,10 @@
|
|||
// return status to the reply button after posting a reply
|
||||
this.refs.node.querySelector('.status-toolbar-reply-button').focus({ preventScroll: true })
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
bookmark () {
|
||||
const { originalStatus, originalStatusId } = this.get()
|
||||
/* no await */ setStatusBookmarkedOrUnbookmarked(originalStatusId, !originalStatus.bookmarked)
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
|
|
|
@ -3,12 +3,13 @@ import { getInCache, hasInCache, statusesCache } from '../cache.js'
|
|||
import { STATUSES_STORE } from '../constants.js'
|
||||
import { cacheStatus } from './cacheStatus.js'
|
||||
import { putStatus } from './insertion.js'
|
||||
import { cloneForStorage } from '../helpers.js'
|
||||
|
||||
//
|
||||
// update statuses
|
||||
//
|
||||
|
||||
async function updateStatus (instanceName, statusId, updateFunc) {
|
||||
async function doUpdateStatus (instanceName, statusId, updateFunc) {
|
||||
const db = await getDatabase(instanceName)
|
||||
if (hasInCache(statusesCache, instanceName, statusId)) {
|
||||
const status = getInCache(statusesCache, instanceName, statusId)
|
||||
|
@ -25,7 +26,7 @@ async function updateStatus (instanceName, statusId, updateFunc) {
|
|||
}
|
||||
|
||||
export async function setStatusFavorited (instanceName, statusId, favorited) {
|
||||
return updateStatus(instanceName, statusId, status => {
|
||||
return doUpdateStatus(instanceName, statusId, status => {
|
||||
const delta = (favorited ? 1 : 0) - (status.favourited ? 1 : 0)
|
||||
status.favourited = favorited
|
||||
status.favourites_count = (status.favourites_count || 0) + delta
|
||||
|
@ -33,7 +34,7 @@ export async function setStatusFavorited (instanceName, statusId, favorited) {
|
|||
}
|
||||
|
||||
export async function setStatusReblogged (instanceName, statusId, reblogged) {
|
||||
return updateStatus(instanceName, statusId, status => {
|
||||
return doUpdateStatus(instanceName, statusId, status => {
|
||||
const delta = (reblogged ? 1 : 0) - (status.reblogged ? 1 : 0)
|
||||
status.reblogged = reblogged
|
||||
status.reblogs_count = (status.reblogs_count || 0) + delta
|
||||
|
@ -41,19 +42,36 @@ export async function setStatusReblogged (instanceName, statusId, reblogged) {
|
|||
}
|
||||
|
||||
export async function setStatusPinned (instanceName, statusId, pinned) {
|
||||
return updateStatus(instanceName, statusId, status => {
|
||||
return doUpdateStatus(instanceName, statusId, status => {
|
||||
status.pinned = pinned
|
||||
})
|
||||
}
|
||||
|
||||
export async function setStatusMuted (instanceName, statusId, muted) {
|
||||
return updateStatus(instanceName, statusId, status => {
|
||||
return doUpdateStatus(instanceName, statusId, status => {
|
||||
status.muted = muted
|
||||
})
|
||||
}
|
||||
|
||||
export async function setStatusBookmarked (instanceName, statusId, bookmarked) {
|
||||
return updateStatus(instanceName, statusId, status => {
|
||||
return doUpdateStatus(instanceName, statusId, status => {
|
||||
status.bookmarked = bookmarked
|
||||
})
|
||||
}
|
||||
|
||||
// For the full list, see https://docs.joinmastodon.org/methods/statuses/#edit
|
||||
const PROPS_THAT_CAN_BE_EDITED = ['content', 'spoiler_text', 'sensitive', 'language', 'media_ids', 'poll']
|
||||
|
||||
export async function updateStatus (instanceName, newStatus) {
|
||||
const clonedNewStatus = cloneForStorage(newStatus)
|
||||
return doUpdateStatus(instanceName, newStatus.id, status => {
|
||||
// We can't use a simple Object.assign() to merge because a prop might have been deleted
|
||||
for (const prop of PROPS_THAT_CAN_BE_EDITED) {
|
||||
if (!(prop in clonedNewStatus)) {
|
||||
delete status[prop]
|
||||
} else {
|
||||
status[prop] = clonedNewStatus[prop]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -8,6 +8,11 @@
|
|||
bind:checked="$neverMarkMediaAsSensitive" on:change="onChange(event)">
|
||||
{intl.showSensitive}
|
||||
</label>
|
||||
<label class="setting-group">
|
||||
<input type="checkbox" id="choice-show-all-spoilers"
|
||||
bind:checked="$showAllSpoilers" on:change="onChange(event)">
|
||||
{intl.showAllSpoilers}
|
||||
</label>
|
||||
<label class="setting-group">
|
||||
<input type="checkbox" id="choice-use-blurhash"
|
||||
bind:checked="$ignoreBlurhash" on:change="onChange(event)">
|
||||
|
|
|
@ -35,6 +35,7 @@ const persistedState = {
|
|||
loggedInInstances: {},
|
||||
loggedInInstancesInOrder: [],
|
||||
markMediaAsSensitive: false,
|
||||
showAllSpoilers: false,
|
||||
neverMarkMediaAsSensitive: false,
|
||||
ignoreBlurhash: false,
|
||||
omitEmojiInDisplayNames: undefined,
|
||||
|
|
|
@ -51,7 +51,7 @@ async function _fetch (url, fetchOptions, options) {
|
|||
async function _putOrPostOrPatch (method, url, body, headers, options) {
|
||||
const fetchOptions = makeFetchOptions(method, headers, options)
|
||||
if (body) {
|
||||
if (body instanceof FormData) {
|
||||
if (body instanceof FormData || body instanceof URLSearchParams) {
|
||||
fetchOptions.body = body
|
||||
} else {
|
||||
fetchOptions.body = JSON.stringify(body)
|
||||
|
|
|
@ -172,15 +172,22 @@ function unmapKeys (keyMap, keys, component) {
|
|||
|
||||
function acceptShortcutEvent (event) {
|
||||
const { target } = event
|
||||
return !(
|
||||
if (
|
||||
event.altKey ||
|
||||
event.metaKey ||
|
||||
event.ctrlKey ||
|
||||
(event.shiftKey && event.key !== '?') || // '?' is a special case - it is allowed
|
||||
(target && (
|
||||
target.isContentEditable ||
|
||||
(event.shiftKey && event.key !== '?') // '?' is a special case - it is allowed
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
// Allow escape everywhere.
|
||||
return true
|
||||
}
|
||||
// Don't allow other keys in text boxes.
|
||||
return !(target && (
|
||||
target.isContentEditable ||
|
||||
['TEXTAREA', 'SELECT'].includes(target.tagName) ||
|
||||
(target.tagName === 'INPUT' && !['radio', 'checkbox'].includes(target.getAttribute('type')))
|
||||
))
|
||||
)
|
||||
))
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { scheduleIdleTask } from './scheduleIdleTask.js'
|
|||
const RETRIES = 5
|
||||
const TIMEOUT = 50
|
||||
|
||||
export async function tryToFocusElement (id) {
|
||||
export async function tryToFocusElement (id, scroll) {
|
||||
for (let i = 0; i < RETRIES; i++) {
|
||||
if (i > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, TIMEOUT))
|
||||
|
@ -13,7 +13,7 @@ export async function tryToFocusElement (id) {
|
|||
const element = document.getElementById(id)
|
||||
if (element) {
|
||||
try {
|
||||
element.focus({ preventScroll: true })
|
||||
element.focus({ preventScroll: !scroll })
|
||||
console.log('focused element', id)
|
||||
return
|
||||
} catch (e) {
|
||||
|
|
|
@ -26,30 +26,28 @@
|
|||
--form-border: #{darken($border-color, 10%)};
|
||||
|
||||
--nav-bg: #{$main-theme-color};
|
||||
--nav-active-bg: #{lighten($main-theme-color, 9%)};
|
||||
--nav-active-bg: #{lighten($main-theme-color, 3%)};
|
||||
--nav-border: #{darken($main-theme-color, 10%)};
|
||||
--nav-a-border: #{$main-theme-color};
|
||||
--nav-a-selected-border: #{$secondary-text-color};
|
||||
--nav-a-selected-bg: #{lighten($main-theme-color, 10%)};
|
||||
--nav-a-selected-active-bg: #{lighten($main-theme-color, 17%)};
|
||||
--nav-a-selected-bg: #{lighten($main-theme-color, 3%)};
|
||||
--nav-a-selected-active-bg: var(--nav-a-selected-bg-hover);
|
||||
--nav-svg-fill: #{$secondary-text-color};
|
||||
--nav-text-color: #{$secondary-text-color};
|
||||
--nav-indicator-bg: #{rgba($secondary-text-color, 0.8)};
|
||||
--nav-indicator-bg-hover: #{rgba($secondary-text-color, 0.85)};
|
||||
--nav-indicator-bg: #{$main-theme-color};
|
||||
--nav-indicator-bg-active: #{mix($secondary-text-color, $main-theme-color, 90%)};
|
||||
--nav-indicator-bg-hover: #{mix($secondary-text-color, $main-theme-color, 60%)};
|
||||
|
||||
--nav-a-selected-border-hover: #{$secondary-text-color};
|
||||
--nav-a-selected-bg-hover: #{lighten($main-theme-color, 15%)};
|
||||
--nav-a-bg-hover: #{lighten($main-theme-color, 5%)};
|
||||
--nav-a-border-hover: #{$main-theme-color};
|
||||
--nav-a-selected-bg-hover: #{lighten($main-theme-color, 4.5%)};
|
||||
--nav-a-bg-hover: #{lighten($main-theme-color, 1.5%)};
|
||||
--nav-svg-fill-hover: #{$secondary-text-color};
|
||||
--nav-text-color-hover: #{$secondary-text-color};
|
||||
|
||||
--action-button-fill-color: #{lighten($main-theme-color, 18%)};
|
||||
--action-button-fill-color-hover: #{lighten($main-theme-color, 22%)};
|
||||
--action-button-fill-color-active: #{lighten($main-theme-color, 5%)};
|
||||
--action-button-fill-color-pressed: #{darken($main-theme-color, 7%)};
|
||||
--action-button-fill-color-pressed-hover: #{darken($main-theme-color, 2%)};
|
||||
--action-button-fill-color-pressed-active: #{darken($main-theme-color, 15%)};
|
||||
--action-button-fill-color: #{lighten($main-theme-color, 11.5%)};
|
||||
--action-button-fill-color-hover: #{lighten($main-theme-color, 6%)};
|
||||
--action-button-fill-color-active: #{$main-theme-color};
|
||||
--action-button-fill-color-pressed: #{darken(saturate($main-theme-color, 5%), 6%)};
|
||||
--action-button-fill-color-pressed-hover: #{darken(saturate($main-theme-color, 5%), 12%)};
|
||||
--action-button-fill-color-pressed-active: #{darken(saturate($main-theme-color, 5%), 15%)};
|
||||
|
||||
--action-button-deemphasized-fill-color: #{$deemphasized-color};
|
||||
--action-button-deemphasized-fill-color-hover: #{lighten($deemphasized-color, 22%)};
|
||||
|
@ -83,8 +81,8 @@
|
|||
--deemphasized-text-color: #{$deemphasized-color};
|
||||
--focus-outline: #{$focus-outline};
|
||||
|
||||
--very-deemphasized-link-color: #{rgba($anchor-color, 0.6)};
|
||||
--very-deemphasized-text-color: #{rgba(#666, 0.6)};
|
||||
--very-deemphasized-text-color: #757575;
|
||||
--very-deemphasized-link-color: var(--very-deemphasized-text-color);
|
||||
|
||||
--status-direct-background: #{darken($body-bg-color, 5%)};
|
||||
--main-theme-color: #{$main-theme-color};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
:root {
|
||||
$deemphasized-color: lighten($main-bg-color, 45%);
|
||||
$deemphasized-color: lighten($main-bg-color, 54%);
|
||||
|
||||
--action-button-deemphasized-fill-color: #{$deemphasized-color};
|
||||
--action-button-deemphasized-fill-color-hover: #{lighten($deemphasized-color, 22%)};
|
||||
|
@ -12,8 +12,8 @@
|
|||
|
||||
--deemphasized-text-color: #{$deemphasized-color};
|
||||
|
||||
--very-deemphasized-link-color: #{rgba($anchor-color, 0.8)};
|
||||
--very-deemphasized-text-color: #{lighten($main-bg-color, 32%)};
|
||||
--very-deemphasized-text-color: #{lighten($main-bg-color, 44%)};
|
||||
--very-deemphasized-link-color: var(--very-deemphasized-text-color);
|
||||
|
||||
--status-direct-background: #{darken($body-bg-color, 5%)};
|
||||
--main-theme-color: #{$main-theme-color};
|
||||
|
|
|
@ -32,7 +32,6 @@ $compose-background: lighten($main-theme-color, 32%);
|
|||
--nav-text-color: #{$main-text-color};
|
||||
--nav-svg-fill-hover: #{$main-text-color};
|
||||
--nav-text-color-hover: #{$main-text-color};
|
||||
--nav-a-selected-border: #{$anchor-color};
|
||||
--nav-a-selected-border-hover: #{$anchor-color};
|
||||
|
||||
accent-color: #{lighten($main-theme-color, 15%)};
|
||||
|
|
|
@ -24,4 +24,6 @@ $compose-background: lighten($main-theme-color, 52%);
|
|||
--action-button-fill-color-pressed: #{darken($anchor-color, 7%)};
|
||||
--action-button-fill-color-pressed-hover: #{darken($anchor-color, 2%)};
|
||||
--action-button-fill-color-pressed-active: #{darken($anchor-color, 15%)};
|
||||
|
||||
--nav-indicator-bg: #{$main-theme-color}; // special override on the nav indicator color
|
||||
}
|
||||
|
|
|
@ -12,3 +12,10 @@ $compose-background: lighten($main-theme-color, 17%);
|
|||
|
||||
@import "_base.scss";
|
||||
@import "_light_scrollbars.scss";
|
||||
|
||||
:root {
|
||||
// make the action buttons a bit lighter
|
||||
--action-button-fill-color: #{lighten($main-theme-color, 17%)};
|
||||
--action-button-fill-color-hover: #{lighten($main-theme-color, 10%)};
|
||||
--action-button-fill-color-active: #{lighten($main-theme-color, 5%)};
|
||||
}
|
||||
|
|
|
@ -15,3 +15,7 @@ $compose-background: lighten($main-theme-color, 52%);
|
|||
@import "_dark.scss";
|
||||
@import "_dark_navbar.scss";
|
||||
@import "_dark_scrollbars.scss";
|
||||
|
||||
:root {
|
||||
--nav-indicator-bg: #{$main-theme-color}; // special override on the nav indicator color
|
||||
}
|
||||
|
|
|
@ -11,4 +11,11 @@ $focus-outline: lighten($main-theme-color, 30%);
|
|||
$compose-background: lighten($main-theme-color, 32%);
|
||||
|
||||
@import "_base.scss";
|
||||
@import "_light_scrollbars.scss";
|
||||
@import "_light_scrollbars.scss";
|
||||
|
||||
:root {
|
||||
// make the action buttons a bit lighter
|
||||
--action-button-fill-color: #{lighten($main-theme-color, 17%)};
|
||||
--action-button-fill-color-hover: #{lighten($main-theme-color, 10%)};
|
||||
--action-button-fill-color-active: #{lighten($main-theme-color, 5%)};
|
||||
}
|
||||
|
|
|
@ -23,6 +23,11 @@ $compose-background: darken($main-theme-color, 12%);
|
|||
--button-primary-bg-hover: #56a7e1;
|
||||
--button-primary-border: transparent;
|
||||
|
||||
|
||||
--action-button-fill-color: #{lighten($main-theme-color, 30%)};
|
||||
--action-button-fill-color-hover: #{lighten($main-theme-color, 36%)};
|
||||
--action-button-fill-color-active: #{lighten($main-theme-color, 42%)};
|
||||
--action-button-fill-color-pressed: #2b90d9;
|
||||
--action-button-fill-color-pressed-hover: #2b90d9;
|
||||
--action-button-fill-color-pressed-hover: #{darken(#2b90d9, 6%)};
|
||||
--action-button-fill-color-pressed-active: #{darken(#2b90d9, 12%)};
|
||||
}
|
||||
|
|
|
@ -12,4 +12,10 @@ $compose-background: darken($main-theme-color, 12%);
|
|||
|
||||
@import "_base.scss";
|
||||
@import "_dark.scss";
|
||||
@import "_dark_scrollbars.scss";
|
||||
@import "_dark_scrollbars.scss";
|
||||
|
||||
:root {
|
||||
--action-button-fill-color-pressed: #{lighten(saturate($main-theme-color, 25%), 8%)};
|
||||
--action-button-fill-color-pressed-hover: #{lighten(saturate($main-theme-color, 25%), 12%)};
|
||||
--action-button-fill-color-pressed-active: #{lighten(saturate($main-theme-color, 25%), 15%)};
|
||||
}
|
||||
|
|
|
@ -33,10 +33,12 @@ $compose-background: darken($main-theme-color, 12%);
|
|||
--form-bg: #{$body-bg-color};
|
||||
--form-border: #{darken($border-color, 10%)};
|
||||
|
||||
--action-button-fill-color: #{lighten($main-theme-color, 20%)};
|
||||
--action-button-fill-color-hover: #{lighten($main-theme-color, 30%)};
|
||||
--action-button-fill-color-active: #{darken($main-theme-color, 40%)};
|
||||
--action-button-fill-color: #{lighten($main-theme-color, 50%)};
|
||||
--action-button-fill-color-hover: #{lighten($main-theme-color, 60%)};
|
||||
--action-button-fill-color-active: #{darken($main-theme-color, 70%)};
|
||||
--action-button-fill-color-pressed: #{lighten($main-theme-color, 85%)};
|
||||
--action-button-fill-color-pressed-hover: #{lighten($main-theme-color, 100%)};
|
||||
--action-button-fill-color-pressed-active: #{lighten($main-theme-color, 80%)};
|
||||
|
||||
--svg-fill: #{lighten($main-theme-color, 50%)};
|
||||
}
|
||||
|
|
|
@ -15,3 +15,7 @@ $compose-background: lighten($main-theme-color, 52%);
|
|||
@import "_dark.scss";
|
||||
@import "_dark_navbar.scss";
|
||||
@import "_dark_scrollbars.scss";
|
||||
|
||||
:root {
|
||||
--nav-indicator-bg: #{$main-theme-color}; // special override on the nav indicator color
|
||||
}
|
||||
|
|
|
@ -18,4 +18,5 @@ $compose-background: lighten($main-theme-color, 52%);
|
|||
|
||||
:root {
|
||||
accent-color: #{darken($main-theme-color, 5%)};
|
||||
--nav-indicator-bg: #{$main-theme-color}; // special override on the nav indicator color
|
||||
}
|
||||
|
|
|
@ -21,9 +21,9 @@
|
|||
//
|
||||
|
||||
--nav-font-size: 1rem;
|
||||
--nav-indicator-height: 2px;
|
||||
--nav-indicator-height: 3px;
|
||||
--nav-border-bottom: 0px;
|
||||
--nav-icon-pad-v: 15px;
|
||||
--nav-icon-pad-v: 14px;
|
||||
--nav-icon-pad-h: 20px;
|
||||
--nav-icon-size: 20px;
|
||||
|
||||
|
@ -46,10 +46,9 @@
|
|||
--main-border-size: 1px;
|
||||
|
||||
@media (max-width: 991px) {
|
||||
--nav-icon-pad-v: 20px;
|
||||
--nav-icon-pad-v: 18px;
|
||||
--nav-icon-pad-h: 10px;
|
||||
--nav-icon-size: 25px;
|
||||
--nav-indicator-height: 3px;
|
||||
--nav-border-bottom: 0px;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ import {
|
|||
} from '../__sapper__/service-worker.js'
|
||||
import { get, post } from './routes/_utils/ajax.js'
|
||||
import { setWebShareData, closeKeyValIDBConnection } from './routes/_database/webShare.js'
|
||||
import { getKnownInstances } from './routes/_database/knownInstances.js'
|
||||
import { basename } from './routes/_api/utils.js'
|
||||
|
||||
const timestamp = process.env.SAPPER_TIMESTAMP
|
||||
const ASSETS = `assets_${timestamp}`
|
||||
|
@ -169,8 +171,18 @@ self.addEventListener('fetch', event => {
|
|||
self.addEventListener('push', event => {
|
||||
event.waitUntil((async () => {
|
||||
const data = event.data.json()
|
||||
const { origin } = event.target
|
||||
// If there is only once instance, then we know for sure that the push notification came from it
|
||||
const knownInstances = await getKnownInstances()
|
||||
if (knownInstances.length !== 1) {
|
||||
// TODO: Mastodon currently does not tell us which instance the push notification came from.
|
||||
// So we have to guess and currently just choose the first one. We _could_ locally store the instance that
|
||||
// currently has push notifications enabled, but this would only work for one instance at a time.
|
||||
// See: https://github.com/mastodon/mastodon/issues/22183
|
||||
await showSimpleNotification(data)
|
||||
return
|
||||
}
|
||||
|
||||
const origin = basename(knownInstances[0])
|
||||
try {
|
||||
const notification = await get(`${origin}/api/v1/notifications/${data.notification_id}`, {
|
||||
Authorization: `Bearer ${data.access_token}`
|
||||
|
@ -185,8 +197,10 @@ self.addEventListener('push', event => {
|
|||
|
||||
async function showSimpleNotification (data) {
|
||||
await self.registration.showNotification(data.title, {
|
||||
badge: '/icon-push-badge.png',
|
||||
icon: data.icon,
|
||||
body: data.body,
|
||||
tag: data.notification_id,
|
||||
data: {
|
||||
url: `${self.origin}/notifications`
|
||||
}
|
||||
|
@ -201,6 +215,8 @@ async function showRichNotification (data, notification) {
|
|||
|
||||
switch (notification.type) {
|
||||
case 'follow':
|
||||
case 'follow_request':
|
||||
case 'admin.report':
|
||||
case 'admin.sign_up': {
|
||||
await self.registration.showNotification(data.title, {
|
||||
badge,
|
||||
|
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 1792"><path fill="#fff" d="M1344 1504q0 13-9 23t-23 9H352q-8 0-13-2t-9-7-6-8-3-11-1-12V896H128q-26 0-45-19t-19-45q0-24 15-41l320-384q19-22 49-22t49 22l320 384q15 17 15 41 0 26-19 45t-45 19H576v384h576q16 0 25 11l160 192q7 10 7 21zm640-416q0 24-15 41l-320 384q-20 23-49 23t-49-23l-320-384q-15-17-15-41 0-26 19-45t45-19h192V640H896q-16 0-25-12L711 436q-7-9-7-20 0-13 10-22t22-10h960q8 0 14 2t9 7 5 8 3 12 1 11v600h192q26 0 45 19t19 45z"/></svg>
|
||||
<svg viewBox="0 0 1792 1792" width="1792" height="1792" xmlns:svg="http://www.w3.org/2000/svg"><path fill="#fff" d="m 384.00001,344.0625 a 71.966714,71.966714 0 0 0 -56.18749,27 l -256.000008,320 a 71.966714,71.966714 0 0 0 56.187498,116.875 h 104.0625 V 1376 a 71.966714,71.966714 0 0 0 71.9375,71.9375 H 1024 a 71.966714,71.966714 0 0 0 56.1875,-116.875 l -128,-160 a 71.966714,71.966714 0 0 0 -56.18749,-27 h -360.0625 v -336.125 h 104.0625 a 71.966714,71.966714 0 0 0 56.18748,-116.875 l -256,-320 a 71.966714,71.966714 0 0 0 -56.18748,-27 z m 384,0 a 71.966714,71.966714 0 0 0 -56.18749,116.875 l 128,160 a 71.966714,71.966714 0 0 0 56.18749,27 h 360.06249 v 336.125 H 1152 a 71.966714,71.966714 0 0 0 -56.1875,116.875 l 256,320 a 71.966714,71.966714 0 0 0 112.375,0 l 256,-320 A 71.966714,71.966714 0 0 0 1664,984.0625 H 1559.9375 V 416 A 71.966714,71.966714 0 0 0 1488,344.0625 Z" /></svg>
|
||||
|
|
Przed Szerokość: | Wysokość: | Rozmiar: 500 B Po Szerokość: | Wysokość: | Rozmiar: 897 B |
Plik diff jest za duży
Load Diff
Plik binarny nie jest wyświetlany.
|
@ -2,7 +2,7 @@ import { favoriteStatus } from '../src/routes/_api/favorite.js'
|
|||
import fetch from 'node-fetch'
|
||||
import FileApi from 'file-api'
|
||||
import { users } from './users.js'
|
||||
import { postStatus } from '../src/routes/_api/statuses.js'
|
||||
import { postStatus, putStatus } from '../src/routes/_api/statuses.js'
|
||||
import { deleteStatus } from '../src/routes/_api/delete.js'
|
||||
import { authorizeFollowRequest, getFollowRequests } from '../src/routes/_api/followRequests.js'
|
||||
import { followAccount, unfollowAccount } from '../src/routes/_api/follow.js'
|
||||
|
@ -33,6 +33,11 @@ export async function postAs (username, text) {
|
|||
null, null, false, null, 'public')
|
||||
}
|
||||
|
||||
export async function putAs (username, text, statusId) {
|
||||
return putStatus(instanceName, users[username].accessToken, statusId, text,
|
||||
null, null, false, null, 'public')
|
||||
}
|
||||
|
||||
export async function postWithSpoilerAndPrivacyAs (username, text, spoiler, privacy) {
|
||||
return postStatus(instanceName, users[username].accessToken, text,
|
||||
null, null, true, spoiler, privacy)
|
||||
|
|
|
@ -35,3 +35,10 @@ test('shows direct vs followers-only vs regular in notifications', async t => {
|
|||
.eql('Cannot be boosted because this is a direct message')
|
||||
.expect($(`${getNthStatusSelector(5)} .status-toolbar button:nth-child(2)`).hasAttribute('disabled')).ok()
|
||||
})
|
||||
|
||||
test('hides status toolbar on notification page', async t => {
|
||||
await loginAsFoobar(t)
|
||||
await t
|
||||
.navigateTo('/notifications')
|
||||
.expect($(`${getNthStatusSelector(1)} .status-toolbar`).exists).notOk()
|
||||
})
|
||||
|
|
|
@ -2,7 +2,7 @@ import { loginAsFoobar } from '../roles'
|
|||
import {
|
||||
generalSettingsButton,
|
||||
getNthShowOrHideButton,
|
||||
getNthStatus, getNthStatusRelativeDateTime, homeNavButton,
|
||||
getNthStatus, getNthStatusAndSensitiveButton, getNthStatusRelativeDateTime, homeNavButton,
|
||||
notificationsNavButton,
|
||||
scrollToStatus,
|
||||
settingsNavButton
|
||||
|
@ -39,6 +39,7 @@ test('aria-labels for CWed statuses', async t => {
|
|||
.expect(getNthStatus(1 + kittenIdx).getAttribute('aria-label')).match(
|
||||
/foobar, Content warning: kitten CW, .* ago, @foobar, Public/i
|
||||
)
|
||||
// toggle the CW button
|
||||
.click(getNthShowOrHideButton(1 + kittenIdx))
|
||||
.expect(getNthStatus(1 + kittenIdx).getAttribute('aria-label')).match(
|
||||
/foobar, here's a kitten with a CW, .* ago, @foobar, Public/i
|
||||
|
@ -47,6 +48,26 @@ test('aria-labels for CWed statuses', async t => {
|
|||
.expect(getNthStatus(1 + kittenIdx).getAttribute('aria-label')).match(
|
||||
/foobar, Content warning: kitten CW, .* ago, @foobar, Public/i
|
||||
)
|
||||
// toggle the "show sensitive media" button
|
||||
.click(getNthStatusAndSensitiveButton(1 + kittenIdx, 1))
|
||||
.expect(getNthStatus(1 + kittenIdx).getAttribute('aria-label')).match(
|
||||
/foobar, Content warning: kitten CW, has media, kitten, .* ago, @foobar, Public/i
|
||||
)
|
||||
.click(getNthStatusAndSensitiveButton(1 + kittenIdx, 1))
|
||||
.expect(getNthStatus(1 + kittenIdx).getAttribute('aria-label')).match(
|
||||
/foobar, Content warning: kitten CW, .* ago, @foobar, Public/i
|
||||
)
|
||||
})
|
||||
|
||||
test('aria-labels for two media attachments', async t => {
|
||||
await loginAsFoobar(t)
|
||||
const twoKittensIdx = homeTimeline.findIndex(_ => _.content === 'here\'s 2 kitten photos')
|
||||
await scrollToStatus(t, 1 + twoKittensIdx)
|
||||
await t
|
||||
.hover(getNthStatus(1 + twoKittensIdx))
|
||||
.expect(getNthStatus(1 + twoKittensIdx).getAttribute('aria-label')).match(
|
||||
/foobar, here's 2 kitten photos, has media, kitten, kitten, .* ago, @foobar, Public/i
|
||||
)
|
||||
})
|
||||
|
||||
test('aria-labels for notifications', async t => {
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
getFirstModalMedia,
|
||||
getNthStatusAccountLink,
|
||||
getNthStatusAccountLinkSelector,
|
||||
focus
|
||||
focus, getNthComposeReplyInput, getActiveElementId, getActiveElementClassList
|
||||
} from '../utils'
|
||||
import { homeTimeline } from '../fixtures'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
|
@ -234,3 +234,19 @@ test('Shortcut down makes next status active when focused inside of a status', a
|
|||
.pressKey('down')
|
||||
.expect(isNthStatusActive(2)()).ok()
|
||||
})
|
||||
|
||||
test('Press r to reply, press Esc to close reply', async t => {
|
||||
await loginAsFoobar(t)
|
||||
await t
|
||||
.expect(getNthStatus(1).exists).ok()
|
||||
await activateStatus(t, 0)
|
||||
const id = await getActiveElementId()
|
||||
await t
|
||||
.expect(getNthComposeReplyInput(1).exists).notOk()
|
||||
.pressKey('r')
|
||||
.expect(getNthComposeReplyInput(1).exists).ok()
|
||||
.expect(getActiveElementClassList()).contains('compose-box-input')
|
||||
.pressKey('esc')
|
||||
.expect(getNthComposeReplyInput(1).exists).notOk()
|
||||
.expect(getActiveElementId()).eql(id)
|
||||
})
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import {
|
||||
closeDialogButton,
|
||||
composeModalInput,
|
||||
getNthFavoritedLabel,
|
||||
getNthStatus,
|
||||
getUrl, modalDialog, notificationsNavButton,
|
||||
isNthStatusActive, goBack
|
||||
isNthStatusActive, goBack,
|
||||
getNthFavoritedLabel
|
||||
} from '../utils'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
|
||||
|
@ -12,16 +12,22 @@ fixture`026-shortcuts-notification.js`
|
|||
.page`http://localhost:4002`
|
||||
|
||||
test('Shortcut f toggles favorite status in notification', async t => {
|
||||
const idx = 0
|
||||
const idx = 6 // "hello foobar"
|
||||
await loginAsFoobar(t)
|
||||
await t
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
.click(notificationsNavButton)
|
||||
.expect(getUrl()).contains('/notifications')
|
||||
.expect(getNthStatus(1 + idx).exists).ok({ timeout: 30000 })
|
||||
.expect(getNthStatus(1).exists).ok({ timeout: 30000 })
|
||||
|
||||
for (let i = 0; i < idx + 1; i++) {
|
||||
await t.pressKey('j')
|
||||
.expect(getNthStatus(1 + i).exists).ok()
|
||||
.expect(isNthStatusActive(1 + i)()).ok()
|
||||
}
|
||||
|
||||
await t
|
||||
.expect(getNthFavoritedLabel(1 + idx)).eql('Favorite')
|
||||
.pressKey('j '.repeat(idx + 1))
|
||||
.expect(isNthStatusActive(1 + idx)()).ok()
|
||||
.pressKey('f')
|
||||
.expect(getNthFavoritedLabel(1 + idx)).eql('Unfavorite')
|
||||
.pressKey('f')
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import {
|
||||
getUrl,
|
||||
scrollToStatus,
|
||||
getNthStatusSpoiler,
|
||||
settingsNavButton,
|
||||
generalSettingsButton,
|
||||
homeNavButton,
|
||||
getNthStatus,
|
||||
getNthShowOrHideButton
|
||||
} from '../utils'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
import { homeTimeline } from '../fixtures.js'
|
||||
import { Selector as $ } from 'testcafe'
|
||||
|
||||
fixture`043-content-warnings.js`
|
||||
.page`http://localhost:4002`
|
||||
|
||||
test('Can set content warnings to auto-expand', async t => {
|
||||
await loginAsFoobar(t)
|
||||
await t
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
.click(settingsNavButton)
|
||||
.click(generalSettingsButton)
|
||||
.click($('#choice-show-all-spoilers'))
|
||||
.click(homeNavButton)
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
.expect(getNthStatus(1).exists).ok()
|
||||
const idx = homeTimeline.findIndex(_ => _.spoiler === 'kitten CW')
|
||||
await scrollToStatus(t, idx + 1)
|
||||
await t
|
||||
.expect(getNthStatusSpoiler(1 + idx).innerText).contains('kitten CW')
|
||||
.expect(getNthStatus(1 + idx).innerText).contains('here\'s a kitten with a CW')
|
||||
.click(getNthShowOrHideButton(1 + idx))
|
||||
.expect(getNthStatus(1 + idx).innerText).notContains('here\'s a kitten with a CW')
|
||||
.click(getNthShowOrHideButton(1 + idx))
|
||||
.expect(getNthStatus(1 + idx).innerText).contains('here\'s a kitten with a CW')
|
||||
})
|
|
@ -37,10 +37,10 @@ test('External links, hashtags, and mentions have correct attributes', async t =
|
|||
.expect(nthAnchor(3).getAttribute('href')).eql('/tags/tag')
|
||||
.expect(nthAnchor(3).hasAttribute('rel')).notOk()
|
||||
.expect(nthAnchor(3).hasAttribute('target')).notOk()
|
||||
.expect(nthAnchor(4).getAttribute('href')).eql('/tags/anotherTag')
|
||||
.expect(nthAnchor(4).getAttribute('href')).eql('/tags/anothertag')
|
||||
.expect(nthAnchor(4).hasAttribute('rel')).notOk()
|
||||
.expect(nthAnchor(4).hasAttribute('target')).notOk()
|
||||
.expect(nthAnchor(5).getAttribute('href')).eql('/tags/yetAnotherTag')
|
||||
.expect(nthAnchor(5).getAttribute('href')).eql('/tags/yetanothertag')
|
||||
.expect(nthAnchor(5).hasAttribute('rel')).notOk()
|
||||
.expect(nthAnchor(5).hasAttribute('target')).notOk()
|
||||
.expect(nthAnchor(6).getAttribute('href')).eql('http://example.com')
|
||||
|
|
|
@ -2,8 +2,16 @@ import { loginAsLockedAccount } from '../roles'
|
|||
import { followAs, unfollowAs } from '../serverActions'
|
||||
import {
|
||||
avatarInComposeBox,
|
||||
communityNavButton, followersButton, getNthSearchResult, getSearchResultByHref, getUrl, goBack,
|
||||
homeNavButton, sleep
|
||||
communityNavButton,
|
||||
followersButton,
|
||||
getNthSearchResult,
|
||||
getNthStatus,
|
||||
getSearchResultByHref,
|
||||
getUrl,
|
||||
goBack,
|
||||
homeNavButton,
|
||||
notificationsNavButton,
|
||||
sleep
|
||||
} from '../utils'
|
||||
import { users } from '../users'
|
||||
import { Selector as $ } from 'testcafe'
|
||||
|
@ -93,6 +101,9 @@ test('Shows unresolved follow requests', async t => {
|
|||
|
||||
await t
|
||||
.expect(communityNavButton.getAttribute('aria-label')).eql('Community (2 follow requests)')
|
||||
.click(notificationsNavButton)
|
||||
.expect(getUrl()).contains('/notifications')
|
||||
.expect(getNthStatus(1).innerText).contains('requested to follow you')
|
||||
.click(communityNavButton)
|
||||
.expect(requestsButton.innerText).contains('Follow requests (2)')
|
||||
.click(requestsButton)
|
||||
|
|
|
@ -4,10 +4,10 @@ import {
|
|||
getNthPinnedStatusFavoriteButton,
|
||||
getNthStatus, getNthStatusContent,
|
||||
getNthStatusOptionsButton, getUrl, homeNavButton, postStatusButton, scrollToTop, scrollToBottom,
|
||||
settingsNavButton, sleep
|
||||
settingsNavButton, sleep, getNthStatusAccountLink
|
||||
} from '../utils'
|
||||
import { users } from '../users'
|
||||
import { postAs } from '../serverActions'
|
||||
import { postAs, postStatusWithMediaAs } from '../serverActions'
|
||||
|
||||
fixture`117-pin-unpin.js`
|
||||
.page`http://localhost:4002`
|
||||
|
@ -84,3 +84,22 @@ test('Saved pinned/unpinned state of status', async t => {
|
|||
.click(getNthStatusOptionsButton(1))
|
||||
.expect(getNthDialogOptionsOption(2).innerText).contains('Unpin from profile', { timeout })
|
||||
})
|
||||
|
||||
test('pinned posts and aria-labels', async t => {
|
||||
const timeout = 20000
|
||||
await postStatusWithMediaAs('foobar', 'here is a sensitive kitty', 'kitten2.jpg', 'kitten', true)
|
||||
await loginAsFoobar(t)
|
||||
await t
|
||||
.expect(getNthStatusContent(1).innerText).contains('here is a sensitive kitty', { timeout })
|
||||
.click(getNthStatusOptionsButton(1))
|
||||
.expect(getNthDialogOptionsOption(2).innerText).contains('Pin to profile')
|
||||
.click(getNthDialogOptionsOption(2))
|
||||
.click(getNthStatusAccountLink(1))
|
||||
.expect(getNthPinnedStatus(1).getAttribute('aria-label')).match(
|
||||
/foobar, here is a sensitive kitty, has media, (.+ ago|just now), @foobar, Public/i
|
||||
)
|
||||
.expect(getNthStatusContent(1).innerText).contains('here is a sensitive kitty')
|
||||
.click(getNthStatusOptionsButton(1))
|
||||
.expect(getNthDialogOptionsOption(2).innerText).contains('Unpin from profile')
|
||||
await sleep(2000)
|
||||
})
|
||||
|
|
|
@ -11,7 +11,7 @@ test('aria-labels for statuses with no content text', async t => {
|
|||
await t
|
||||
.hover(getNthStatus(1))
|
||||
.expect(getNthStatus(1).getAttribute('aria-label')).match(
|
||||
/foobar, has media, (.+ ago|just now), @foobar, Public/i
|
||||
/foobar, has media, kitteh, (.+ ago|just now), @foobar, Public/i
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
sleep,
|
||||
getNthStatusPollRefreshButton,
|
||||
getNthStatusPollVoteCount,
|
||||
getNthStatusRelativeDate, getUrl, goBack, getNthStatusSpoiler, getNthShowOrHideButton
|
||||
getNthStatusRelativeDate, getUrl, goBack, getNthStatusSpoiler, getNthShowOrHideButton, getNthStatusPollExpiry
|
||||
} from '../utils'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
import { createPollAs, voteOnPollAs } from '../serverActions'
|
||||
|
@ -22,6 +22,7 @@ test('Can vote on polls', async t => {
|
|||
await t
|
||||
.expect(getNthStatusContent(1).innerText).contains('vote on my cool poll')
|
||||
.expect(getNthStatusPollVoteCount(1).innerText).eql('0 votes')
|
||||
.expect(getNthStatusPollExpiry(1).innerText).match(/Ends in .*/)
|
||||
await sleep(1000)
|
||||
await t
|
||||
.click(getNthStatusPollOption(1, 2))
|
||||
|
@ -32,6 +33,7 @@ test('Can vote on polls', async t => {
|
|||
.expect(getNthStatusPollResult(1, 1).innerText).eql('0% yes')
|
||||
.expect(getNthStatusPollResult(1, 2).innerText).eql('100% no')
|
||||
.expect(getNthStatusPollVoteCount(1).innerText).eql('1 vote')
|
||||
.expect(getNthStatusPollExpiry(1).innerText).match(/Ends in .*/)
|
||||
})
|
||||
|
||||
test('Can vote on multiple-choice polls', async t => {
|
||||
|
|
|
@ -38,7 +38,8 @@ const addFilter = async (t, phrase, tweak) => {
|
|||
.expect(modalDialog.exists).notOk()
|
||||
}
|
||||
|
||||
test('Can filter basic words', async t => {
|
||||
// TODO: test broken by Mastodon v4 bug https://github.com/mastodon/mastodon/issues/21965
|
||||
test.skip('Can filter basic words', async t => {
|
||||
await postAs('admin', 'do not filter me!')
|
||||
await postAs('admin', 'filterMeOut okay!')
|
||||
await postAs('admin', 'filterMeOutTooEvenThoughItIsOneBigWord!')
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import {
|
||||
getNthStatus, getUrl, goBack,
|
||||
sleep
|
||||
} from '../utils'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
import { postAs, putAs } from '../serverActions'
|
||||
|
||||
fixture`140-editing.js`
|
||||
.page`http://localhost:4002`
|
||||
|
||||
test('Edited toots are updated in the UI', async t => {
|
||||
const { id: statusId } = await postAs('admin', 'yolo')
|
||||
await sleep(500)
|
||||
|
||||
await loginAsFoobar(t)
|
||||
await t.expect(getNthStatus(1).innerText).contains('yolo', { timeout: 20000 })
|
||||
|
||||
await putAs('admin', 'wait I mean YOLO', statusId)
|
||||
await sleep(500)
|
||||
|
||||
await t.click(getNthStatus(1))
|
||||
.expect(getUrl()).contains('/statuses')
|
||||
.expect(getNthStatus(1).innerText).contains('wait I mean YOLO', { timeout: 20000 })
|
||||
await goBack()
|
||||
await t
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
.expect(getNthStatus(1).innerText).contains('wait I mean YOLO', { timeout: 20000 })
|
||||
})
|
|
@ -394,6 +394,10 @@ export function getNthStatusPollVoteCount (n) {
|
|||
return $(`${getNthStatusSelector(n)} .poll .poll-stat:nth-child(1) .poll-stat-text`)
|
||||
}
|
||||
|
||||
export function getNthStatusPollExpiry (n) {
|
||||
return $(`${getNthStatusSelector(n)} .poll .poll-stat-expiry`)
|
||||
}
|
||||
|
||||
export function getComposePollNthInput (n) {
|
||||
return $(`.compose-poll input[type="text"]:nth-of-type(${n})`)
|
||||
}
|
||||
|
|
19
vercel.json
19
vercel.json
|
@ -6,15 +6,8 @@
|
|||
"github": {
|
||||
"silent": true
|
||||
},
|
||||
"builds": [
|
||||
{
|
||||
"src": "package.json",
|
||||
"use": "@now/static-build",
|
||||
"config": {
|
||||
"distDir": "__sapper__/export"
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildCommand": "yarn build",
|
||||
"outputDirectory": "__sapper__/export",
|
||||
"routes": [
|
||||
{
|
||||
"src": "^/service-worker\\.js$",
|
||||
|
@ -36,7 +29,13 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"src": "^/.*\\.(png|css|json|svg|jpe?g|map|txt|gz|webapp|woff|woff2)$",
|
||||
"src": "^/.*\\.(png|jpe?g)$",
|
||||
"headers": {
|
||||
"cache-control": "public,max-age=31536000,immutable"
|
||||
}
|
||||
},
|
||||
{
|
||||
"src": "^/.*\\.(css|json|svg|map|txt|gz|webapp|woff|woff2)$",
|
||||
"headers": {
|
||||
"cache-control": "public,max-age=3600"
|
||||
}
|
||||
|
|
|
@ -12,8 +12,9 @@ import urlRegex from '../src/routes/_utils/urlRegexSource.js'
|
|||
// TODO: make it so we don't have to list these out explicitly
|
||||
import fr from 'emoji-picker-element/i18n/fr.js'
|
||||
import de from 'emoji-picker-element/i18n/de.js'
|
||||
import es from 'emoji-picker-element/i18n/es.js'
|
||||
|
||||
const emojiPickerLocales = { fr, de }
|
||||
const emojiPickerLocales = { fr, de, es }
|
||||
|
||||
const emojiPickerI18n = LOCALE !== DEFAULT_LOCALE && emojiPickerLocales[LOCALE]
|
||||
|
||||
|
|
|
@ -2441,9 +2441,9 @@ decamelize@^4.0.0:
|
|||
integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==
|
||||
|
||||
decode-uri-component@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||
integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9"
|
||||
integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==
|
||||
|
||||
dedent@^0.4.0:
|
||||
version "0.4.0"
|
||||
|
|
Ładowanie…
Reference in New Issue