Compare commits

..

2 commits

Author SHA1 Message Date
FloatingGhost ef64d693da remove IHBA link 2022-11-27 21:00:43 +00:00
FloatingGhost 669b3a41ca Add basic cypress tests 2022-11-27 20:56:12 +00:00
97 changed files with 2104 additions and 1644 deletions

View file

@ -1,49 +0,0 @@
name: "Bug report"
about: "Something isn't working as expected"
title: "[bug] "
body:
- type: markdown
attributes:
value: "Thanks for taking the time to file this bug report! Please try to be as specific and detailed as you can, so we can track down the issue and fix it as soon as possible."
- type: input
id: version
attributes:
label: "Version"
description: "Which version of pleroma-fe are you running? If running develop, specify the commit hash."
placeholder: "e.g. 2022.11, 40e86998e6"
- type: textarea
id: attempt
attributes:
label: "What were you trying to do?"
validations:
required: true
- type: textarea
id: expectation
attributes:
label: "What did you expect to happen?"
validations:
required: true
- type: textarea
id: reality
attributes:
label: "What actually happened?"
validations:
required: true
- type: dropdown
id: severity
attributes:
label: "Severity"
description: "Does this issue prevent you from using the software as normal?"
options:
- "I cannot use the software"
- "I cannot use it as easily as I'd like"
- "I can manage"
validations:
required: true
- type: checkboxes
id: searched
attributes:
label: "Have you searched for this issue?"
description: "Please double-check that your issue is not already being tracked on [the forums](https://meta.akkoma.dev) or [the issue tracker](https://akkoma.dev/AkkomaGang/pleroma-fe/issues)."
options:
- label: "I have double-checked and have not found this issue mentioned anywhere."

View file

@ -1,29 +0,0 @@
name: "Feature request"
about: "I'd like something to be added to pleroma-fe"
title: "[feat] "
body:
- type: markdown
attributes:
value: "Thanks for taking the time to request a new feature! Please be as concise and clear as you can in your proposal, so we could understand what you're going for."
- type: textarea
id: idea
attributes:
label: "The idea"
description: "What do you think you should be able to do in pleroma-fe?"
validations:
required: true
- type: textarea
id: reason
attributes:
label: "The reasoning"
description: "Why would this be a worthwhile feature? Does it solve any problems? Have people talked about wanting it?"
validations:
required: true
- type: checkboxes
id: searched
attributes:
label: "Have you searched for this feature request?"
description: "Please double-check that your issue is not already being tracked on [the forums](https://meta.akkoma.dev), [the issue tracker](https://akkoma.dev/AkkomaGang/pleroma-fe/issues), or the one for [the backend](https://akkoma.dev/AkkomaGang/akkoma/issues)."
options:
- label: "I have double-checked and have not found this feature request mentioned anywhere."
- label: "This feature is related to the pleroma-fe Akkoma frontend specifically, and not the backend."

View file

@ -1,22 +1,22 @@
# Akkoma-FE
# Pleroma-FE
![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet)
This is a fork of Akkoma-FE from the Pleroma project, with support for new Akkoma features such as:
This is a fork of Pleroma-FE from the Pleroma project, with support for new Akkoma features such as:
- MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm)
- Custom emoji reactions
# For Translators
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE.
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Pleroma-FE.
Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there.
Akkoma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
Pleroma-FE will set your language by your browser locale, but you can temporarily force it in the code by changing the locale in main.js.
# FOR ADMINS
To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Akkoma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc.
To use Pleroma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/administration/CLI_tasks/frontend/) CLI task to install Pleroma-FE, then modify your configuration as described in the [Frontend Management](https://docs.akkoma.dev/stable/configuration/frontend_management/) doc.
## Build Setup
@ -52,4 +52,4 @@ Edit config.json for configuration.
### Login methods
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.

View file

@ -31,13 +31,15 @@ var devMiddleware = require('webpack-dev-middleware')(compiler, {
var hotMiddleware = require('webpack-hot-middleware')(compiler)
// proxy api requests
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(context, options))
})
if (!process.env.NO_DEV_PROXY) {
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(context, options))
})
}
// handle fallback for HTML5 history API
app.use(require('connect-history-api-fallback')())

4
config/cypress.json Normal file
View file

@ -0,0 +1,4 @@
{
"target": "http://cypress.example.com",
"staticConfigPreference": false
}

View file

@ -1,14 +1,16 @@
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
let settings = {}
const localSettings = process.env.CONFIG || './local.json'
console.log('Using settings', localSettings)
try {
settings = require('./local.json')
settings = require(localSettings)
if (settings.target && settings.target.endsWith('/')) {
// replacing trailing slash since it can conflict with some apis
// and that's how actual BE reports its url
settings.target = settings.target.replace(/\/$/, '')
}
console.log('Using local dev server settings (/config/local.json):')
console.log('Using local dev server settings:')
console.log(JSON.stringify(settings, null, 2))
} catch (e) {
console.log('Local dev server settings not found (/config/local.json)')
@ -38,11 +40,6 @@ module.exports = {
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'/manifest.json': {
target,
changeOrigin: true,
cookieDomainRewrite: 'localhost'
},
'/api': {
target,
changeOrigin: true,

20
cypress.config.js Normal file
View file

@ -0,0 +1,20 @@
const { defineConfig } = require("cypress");
const config = require('./build/webpack.dev.conf');
module.exports = defineConfig({
e2e: {
baseUrl: "http://localhost:8080",
setupNodeEvents(on, config) {
// implement node event listeners here
},
viewportHeight: 1080,
viewportWidth: 1920,
},
component: {
devServer: {
framework: "vue",
bundler: "webpack",
webpackConfig: config
},
},
});

View file

@ -0,0 +1,34 @@
/// <reference types="cypress" />
describe('signing in', () => {
beforeEach(async () => {
cy.clearLocalStorage()
await indexedDB.deleteDatabase('localforage')
})
it('registers an oauth application', async () => {
cy.defaultIntercepts();
cy.intercept('POST', '/oauth/token', { fixture: 'oauth_token.json'}).as('createToken')
cy.intercept('/api/v1/accounts/verify_credentials', { fixture: 'user.json' }).as('verifyCredentials')
cy.visit('/')
cy.wait('@getInstance')
cy.get('input#username').type('testuser');
cy.get('input#password').type('testpassword');
cy.get('button[type="submit"]').click();
cy.wait('@createApp')
cy.wait('@createToken').then((interception) => {
console.log(interception.request)
const form = interception.request.body.split('\r\n---')
cy.expectHtmlFormEntryToBe(form, 'grant_type', 'client_credentials')
});
cy.wait('@createToken').then((interception) => {
console.log(interception.request)
const form = interception.request.body.split('\r\n---')
cy.expectHtmlFormEntryToBe(form, 'grant_type', 'password')
cy.expectHtmlFormEntryToBe(form, 'username', 'testuser')
cy.expectHtmlFormEntryToBe(form, 'password', 'testpassword')
});
cy.wait('@verifyCredentials')
});
})

View file

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View file

@ -0,0 +1,26 @@
{
"masto_fe": {
"showInstanceSpecificPanel": true
},
"pleroma_fe": {
"alwaysShowSubjectInput": true,
"background": "/images/5cm.jpg",
"collapseMessageWithSubject": true,
"formattingOptionsEnabled": true,
"hidePostStats": false,
"hideSiteFavicon": true,
"hideUserStats": true,
"logo": "/static/logo.png",
"logoMask": false,
"redirectRootLogin": "/main/friends",
"redirectRootNoLogin": "/main/public",
"renderMisskeyMarkdown": true,
"scopeCopy": true,
"scopeOptionsEnabled": true,
"showInstanceSpecificPanel": true,
"showNavShortcuts": false,
"showPanelNavShortcuts": true,
"subjectLineBehavior": "email",
"theme": "ihatebeingalive"
}
}

View file

@ -0,0 +1,133 @@
{
"approval_required": false,
"avatar_upload_limit": 2000000,
"background_image": "/images/city.jpg",
"background_upload_limit": 4000000,
"banner_upload_limit": 4000000,
"description": "A Test Instace",
"description_limit": 5000,
"email": "somewhere@example.com",
"languages": [
"en",
"ja"
],
"max_toot_chars": 5000,
"pleroma": {
"metadata": {
"account_activation_required": false,
"features": [
"pleroma_api",
"akkoma_api",
"mastodon_api",
"mastodon_api_streaming",
"polls",
"v2_suggestions",
"pleroma_explicit_addressing",
"shareable_emoji_packs",
"multifetch",
"pleroma:api/v1/notifications:include_types_filter",
"editing",
"media_proxy",
"pleroma_emoji_reactions",
"exposable_reactions",
"profile_directory",
"akkoma:machine_translation",
"custom_emoji_reactions",
"pleroma:get:main/ostatus"
],
"federation": {
"enabled": true,
"exclusions": true,
"mrf_hashtag": {
"federated_timeline_removal": [],
"reject": [],
"sensitive": [
"nsfw"
]
},
"mrf_hellthread": {
"delist_threshold": 5,
"reject_threshold": 10
},
"mrf_keyword": {
"federated_timeline_removal": [],
"reject": [
"rejectme"
],
"replace": []
},
"mrf_policies": [
"SimplePolicy",
"HellthreadPolicy",
"KeywordPolicy",
"TagPolicy",
"InlineQuotePolicy",
"HashtagPolicy"
],
"mrf_simple": {
"accept": [],
"avatar_removal": [],
"banner_removal": [],
"federated_timeline_removal": [],
"followers_only": [],
"media_nsfw": [],
"media_removal": [],
"reject": [
"badinstance.com"
],
"reject_deletes": [],
"report_removal": []
},
"mrf_simple_info": {
"reject": {
"badinstance.com": {
"reason": "This instance is bad"
}
}
},
"quarantined_instances": [],
"quarantined_instances_info": {
"quarantined_instances": {}
}
},
"fields_limits": {
"max_fields": 10,
"max_remote_fields": 20,
"name_length": 512,
"value_length": 2048
},
"post_formats": [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode",
"text/x.misskeymarkdown"
],
"privileged_staff": false
},
"stats": {
"mau": 27
},
"vapid_public_key": "BDgd8xcYuskwMLnr-3Gi-xOU_Jz9IOxhHIW0VWgBMM47wB8qfC_Hw26eNd3sGDSEoXk06ZY-L5qKHqLLNzZSdnw"
},
"poll_limits": {
"max_expiration": 31536000,
"max_option_chars": 200,
"max_options": 20,
"min_expiration": 0
},
"registrations": false,
"stats": {
"domain_count": 14557,
"status_count": 284658,
"user_count": 72
},
"thumbnail": "thumb.png",
"title": "Test Instance",
"upload_limit": 100000000,
"uri": "http://localhost:8080/",
"urls": {
"streaming_api": "ws://localhost:8080"
},
"version": "2.7.2 (compatible; Akkoma 3.4.0-118-g41241bbb-develop)"
}

View file

@ -0,0 +1 @@
<h4>Testing Panel</h4>

View file

@ -0,0 +1,141 @@
{
"metadata": {
"accountActivationRequired": false,
"features": [
"pleroma_api",
"akkoma_api",
"mastodon_api",
"mastodon_api_streaming",
"polls",
"v2_suggestions",
"pleroma_explicit_addressing",
"shareable_emoji_packs",
"multifetch",
"pleroma:api/v1/notifications:include_types_filter",
"editing",
"media_proxy",
"pleroma_emoji_reactions",
"exposable_reactions",
"profile_directory",
"akkoma:machine_translation",
"custom_emoji_reactions",
"pleroma:get:main/ostatus"
],
"federation": {
"enabled": true,
"exclusions": true,
"mrf_hashtag": {
"federated_timeline_removal": [],
"reject": [],
"sensitive": [
"nsfw"
]
},
"mrf_hellthread": {
"delist_threshold": 5,
"reject_threshold": 10
},
"quarantined_instances": [],
"quarantined_instances_info": {
"quarantined_instances": {}
}
},
"fieldsLimits": {
"maxFields": 10,
"maxRemoteFields": 20,
"nameLength": 512,
"valueLength": 2048
},
"invitesEnabled": true,
"localBubbleInstances": [
"bubble.example.com"
],
"mailerEnabled": true,
"nodeDescription": "A Test Instance",
"nodeName": "Test",
"pollLimits": {
"max_expiration": 31536000,
"max_option_chars": 200,
"max_options": 20,
"min_expiration": 0
},
"postFormats": [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode",
"text/x.misskeymarkdown"
],
"private": false,
"restrictedNicknames": [
".well-known",
"~",
"about",
"activities",
"api",
"auth",
"check_password",
"dev",
"friend-requests",
"inbox",
"internal",
"main",
"media",
"nodeinfo",
"notice",
"oauth",
"objects",
"ostatus_subscribe",
"pleroma",
"proxy",
"push",
"registration",
"relay",
"settings",
"status",
"tag",
"user-search",
"user_exists",
"users",
"web",
"verify_credentials",
"update_credentials",
"relationships",
"search",
"confirmation_resend",
"mfa"
],
"skipThreadContainment": true,
"staffAccounts": [
"http://localhost:8080/users/admin"
],
"suggestions": {
"enabled": false
},
"uploadLimits": {
"avatar": 2000000,
"background": 4000000,
"banner": 4000000,
"general": 100000000
}
},
"openRegistrations": false,
"protocols": [
"activitypub"
],
"services": {
"inbound": [],
"outbound": []
},
"software": {
"name": "akkoma",
"version": "3.4.0-118-g41241bbb-develop"
},
"usage": {
"localPosts": 284658,
"users": {
"total": 72
}
},
"version": "2.0"
}

View file

@ -0,0 +1,9 @@
{
"id": "563419",
"name": "test app",
"website": null,
"redirect_uri": "urn:ietf:wg:oauth:2.0:oob",
"client_id": "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
"client_secret": "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
"vapid_key": "BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M="
}

View file

@ -0,0 +1,6 @@
{
"access_token": "ZA-Yj3aBD8U8Cm7lKUp-lm9O9BmDgdhHzDeqsY8tlL0",
"token_type": "Bearer",
"scope": "read write follow push",
"created_at": 1573979017
}

View file

@ -0,0 +1 @@
[]

198
cypress/fixtures/user.json Normal file
View file

@ -0,0 +1,198 @@
{
"acct": "test",
"akkoma": {
"instance": {
"favicon": "favicon.png",
"name": "cypress.example.com",
"nodeinfo": {
"metadata": {
"accountActivationRequired": false,
"features": [
"pleroma_api",
"akkoma_api",
"mastodon_api",
"mastodon_api_streaming",
"polls",
"v2_suggestions",
"pleroma_explicit_addressing",
"shareable_emoji_packs",
"multifetch",
"pleroma:api/v1/notifications:include_types_filter",
"editing",
"media_proxy",
"pleroma_emoji_reactions",
"exposable_reactions",
"profile_directory",
"akkoma:machine_translation",
"custom_emoji_reactions",
"pleroma:get:main/ostatus"
],
"federation": {
"enabled": true,
"exclusions": true
},
"fieldsLimits": {
"maxFields": 10,
"maxRemoteFields": 20,
"nameLength": 512,
"valueLength": 2048
},
"invitesEnabled": true,
"localBubbleInstances": [
"bubble.exaple.com"
],
"mailerEnabled": true,
"nodeDescription": "An Akkoma Instance",
"nodeName": "Test Instance",
"pollLimits": {
"max_expiration": 31536000,
"max_option_chars": 200,
"max_options": 20,
"min_expiration": 0
},
"postFormats": [
"text/plain",
"text/html",
"text/markdown",
"text/bbcode",
"text/x.misskeymarkdown"
],
"private": false,
"restrictedNicknames": [
".well-known",
"~",
"about",
"activities",
"api",
"auth",
"check_password",
"dev",
"friend-requests",
"inbox",
"internal",
"main",
"media",
"nodeinfo",
"notice",
"oauth",
"objects",
"ostatus_subscribe",
"pleroma",
"proxy",
"push",
"registration",
"relay",
"settings",
"status",
"tag",
"user-search",
"user_exists",
"users",
"web",
"verify_credentials",
"update_credentials",
"relationships",
"search",
"confirmation_resend",
"mfa"
],
"skipThreadContainment": true,
"staffAccounts": [
"http://cypress.example.com/users/test"
],
"suggestions": {
"enabled": false
},
"uploadLimits": {
"avatar": 2000000,
"background": 4000000,
"banner": 4000000,
"general": 100000000
}
},
"openRegistrations": false,
"protocols": [
"activitypub"
],
"services": {
"inbound": [],
"outbound": []
},
"software": {
"name": "akkoma",
"version": "3.4.0-136-g98d4d691-develop"
},
"usage": {
"localPosts": 284709,
"users": {
"total": 72
}
},
"version": "2.0"
}
}
},
"avatar": "3ba406a0f1ce44b2379029f322cacb77b78951f515495739bd65bbfcee297931.jpg",
"avatar_static": "3ba406a0f1ce44b2379029f322cacb77b78951f515495739bd65bbfcee297931.jpg",
"bot": false,
"created_at": "2018-11-27T18:04:50.000Z",
"display_name": "Test User",
"emojis": [
],
"fields": [
],
"follow_requests_count": 1,
"followers_count": 1,
"following_count": 1,
"fqn": "test@cypress.example.com",
"header": "header.gif",
"header_static": "header.gif",
"id": "1",
"last_status_at": "2022-11-27T15:17:03",
"locked": true,
"note": "A test user",
"pleroma": {
"allow_following_move": true,
"also_known_as": [],
"ap_id": "http://cypress.example.com/users/example",
"background_image": "something.jpg",
"deactivated": false,
"email": "somewhere@example.com",
"favicon": "/favicon.png",
"hide_favorites": true,
"hide_followers": true,
"hide_followers_count": false,
"hide_follows": true,
"hide_follows_count": false,
"is_admin": true,
"is_confirmed": true,
"is_moderator": true,
"is_suggested": false,
"notification_settings": {
"block_from_strangers": false,
"hide_notification_contents": false
},
"relationship": {},
"settings_store": {},
"skip_thread_containment": false,
"tags": [],
"unread_conversation_count": 2108,
"unread_notifications_count": 18
},
"source": {
"fields": [
],
"note": "Test user",
"pleroma": {
"actor_type": "Person",
"discoverable": true,
"no_rich_text": false,
"show_role": true
},
"privacy": "private",
"sensitive": false
},
"statuses_count": 10166,
"url": "http://cypress.example.com/users/test",
"username": "test"
}

View file

@ -0,0 +1,25 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Components App</title>
</head>
<body>
<div data-cy-root></div>
</body>
</html>

View file

@ -0,0 +1,19 @@
import './commands'
import Vuex from 'vuex'
import getStore from '../../src/store'
import { mount } from 'cypress/vue'
Cypress.Commands.add('mount', (component, options = {}) => {
// Setup options object
options.extensions = options.extensions || {}
options.extensions.plugins = options.extensions.plugins || []
// Use store passed in from options, or initialize a new one
options.store = options.store || getStore()
// Add Vuex plugin
options.extensions.plugins.push(Vuex)
return mount(component, options)
})

48
cypress/support/e2e.js Normal file
View file

@ -0,0 +1,48 @@
// ***********************************************************
// This example support/e2e.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
Cypress.Commands.add('defaultIntercepts', () => {
const notAuthorized = {
body: { error: "not_authorized" },
statusCode: 403
}
cy.intercept('/api/pleroma/frontend_configurations', { fixture: 'frontend_configurations.json' });
cy.intercept('/instance/panel.html', { fixture: 'instance_panel.html' })
cy.intercept('/api/v1/instance', { fixture: 'instance.json' }).as('getInstance')
cy.intercept('/nodeinfo/2.0.json', { fixture: 'nodeinfo.json' })
cy.intercept('/api/v1/timelines/public*', { fixture: 'public_timeline.json' })
cy.intercept('/static/stickers.json', { body: {} })
cy.intercept('POST', 'http://cypress.example.com/oauth/token', notAuthorized);
cy.intercept('/api/v1/mutes', notAuthorized);
cy.intercept('/api/v1/announcements', notAuthorized);
cy.intercept('POST', 'http://cypress.example.com/api/v1/apps', { fixture: 'oauth_app.json' }).as('createApp');
});
Cypress.Commands.add('authenticatedIntercepts', () => {
cy.intercept('POST', '/oauth/token', { fixture: 'oauth_token.json'})
cy.intercept('/api/v1/announcements', { body: [] })
cy.intercept('/api/v1/mutes', { body: [] })
});
Cypress.Commands.add('expectHtmlFormEntryToBe', (form, key, value) => {
expect(form.find((line) => line.includes(`name="${key}"`))).to.include(value)
});

Binary file not shown.

View file

@ -9,11 +9,8 @@
<link rel="stylesheet" href="/static/font/tiresias.css">
<link rel="stylesheet" href="/static/font/css/lato.css">
<link rel="stylesheet" href="/static/mfm.css">
<link rel="stylesheet" href="/static/custom.css">
<link rel="stylesheet" href="/static/theme-holder.css" id="theme-holder">
<!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="manifest" href="/manifest.json">
</head>
<body class="hidden">
<noscript>To use Akkoma, please enable JavaScript.</noscript>

View file

@ -1,6 +1,6 @@
{
"name": "pleroma_fe",
"version": "3.5.0",
"version": "3.2.0",
"description": "A frontend for Akkoma instances",
"author": "Roger Braun <roger@rogerbraun.net>",
"private": true,
@ -13,26 +13,25 @@
"test": "npm run unit && npm run e2e",
"stylelint": "stylelint src/**/*.scss",
"lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs",
"run:cypress": "CONFIG='./cypress.json' NO_DEV_SERVER=true yarn dev"
},
"dependencies": {
"@babel/runtime": "7.17.8",
"@chenfengyuan/vue-qrcode": "2.0.0",
"@floatingghost/pinch-zoom-element": "^1.3.1",
"@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-regular-svg-icons": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "^6.2.0",
"@fortawesome/vue-fontawesome": "3.0.1",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
"@vuelidate/core": "^2.0.0",
"@vuelidate/validators": "^2.0.0",
"blurhash": "^2.0.4",
"body-scroll-lock": "2.7.1",
"chromatism": "3.0.0",
"click-outside-vue3": "4.0.1",
"cropperjs": "1.5.12",
"diff": "3.5.0",
"escape-html": "1.0.3",
"iso-639-1": "^2.1.15",
"js-cookie": "^3.0.1",
"localforage": "1.10.0",
"parse-link-header": "^2.0.0",
@ -68,6 +67,7 @@
"cross-spawn": "^7.0.3",
"css-loader": "^6.7.2",
"custom-event-polyfill": "^1.0.7",
"cypress": "^11.2.0",
"eslint": "^7.32.0",
"eslint-config-standard": "^17.0.0",
"eslint-friendly-formatter": "^4.0.1",
@ -84,6 +84,7 @@
"html-webpack-plugin": "^5.5.0",
"http-proxy-middleware": "0.21.0",
"inject-loader": "2.0.1",
"iso-639-1": "2.1.15",
"isparta-loader": "2.0.0",
"json-loader": "0.5.7",
"karma": "6.3.17",

View file

@ -1,7 +1,6 @@
// stylelint-disable rscss/class-format
@import './_variables.scss';
@import '@fortawesome/fontawesome-svg-core/styles.css';
@import '@floatingghost/pinch-zoom-element/dist/pinch-zoom.css';
:root {
--navbar-height: 3.5rem;
--post-line-height: 1.4;

View file

@ -4,8 +4,6 @@ import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import { config } from '@fortawesome/fontawesome-svg-core';
config.autoAddCss = false
import App from '../App.vue'
import routes from './routes'
@ -395,10 +393,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
getInstanceConfig({ store })
])
// Start fetching things that don't need to block the UI
store.dispatch('fetchMutes')
store.dispatch('startFetchingAnnouncements')
store.dispatch('startFetchingReports')
getTOS({ store })
getStickers({ store })

View file

@ -18,7 +18,6 @@ import {
faPencilAlt,
faAlignRight
} from '@fortawesome/free-solid-svg-icons'
import Blurhash from '../blurhash/Blurhash.vue'
library.add(
faFile,
@ -64,8 +63,7 @@ const Attachment = {
components: {
Flash,
StillImage,
VideoAttachment,
Blurhash
VideoAttachment
},
computed: {
classNames () {
@ -86,9 +84,6 @@ const Attachment = {
useContainFit () {
return this.$store.getters.mergedConfig.useContainFit
},
useBlurhash () {
return this.$store.getters.mergedConfig.useBlurhash
},
placeholderName () {
if (this.attachment.description === '' || !this.attachment.description) {
return this.type.toUpperCase()

View file

@ -64,15 +64,7 @@
:title="attachment.description"
@click.prevent.stop="toggleHidden"
>
<Blurhash
v-if="useBlurhash && attachment.blurhash"
:height="512"
:width="1024"
:hash="attachment.blurhash"
:punch="1"
/>
<img
v-else
:key="nsfwImage"
class="nsfw"
:src="nsfwImage"

View file

@ -21,7 +21,7 @@ const BasicUserCard = {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
userProfileLink (user) {
userProfileLink(user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
}
}

View file

@ -1,66 +0,0 @@
<template>
<canvas
ref="canvas"
class="blurhash"
/>
</template>
<script>
import { decode } from "blurhash";
export default {
name: 'Blurhash',
props: {
hash: {
type: String,
required: true,
},
width: {
type: Number,
required: true,
},
height: {
type: Number,
required: true,
},
punch: {
type: Number,
default: null,
},
},
data() {
return {
canvas: null,
ctx: null,
};
},
mounted() {
this.canvas = this.$refs.canvas;
this.ctx = this.canvas.getContext('2d');
this.canvas.width = 1024;
this.canvas.height = 512;
this.draw();
},
methods: {
draw() {
const pixels = decode(this.hash, this.width, this.height, this.punch);
const imageData = this.ctx.createImageData(this.width, this.height);
imageData.data.set(pixels);
this.ctx.putImageData(imageData, 0, 0);
fetch("/static/blurhash-overlay.png")
.then((response) => response.blob())
.then((blob) => {
const img = new Image();
img.src = URL.createObjectURL(blob);
img.onload = () => {
this.ctx.drawImage(img, 0, 0, this.width, this.height);
};
});
},
}
}
</script>
<style scoped>
</style>

View file

@ -1,133 +0,0 @@
const EMOJI_SIZE = 32 + 8
const GROUP_TITLE_HEIGHT = 24
const BUFFER_SIZE = 3 * EMOJI_SIZE
const EmojiGrid = {
props: {
groups: {
required: true,
type: Array
}
},
data () {
return {
containerWidth: 0,
containerHeight: 0,
scrollPos: 0,
resizeObserver: null
}
},
mounted () {
const rect = this.$refs.container.getBoundingClientRect()
this.containerWidth = rect.width
this.containerHeight = rect.height
this.resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
this.containerWidth = entry.contentRect.width
this.containerHeight = entry.contentRect.height
}
})
this.resizeObserver.observe(this.$refs.container)
},
beforeUnmount () {
this.resizeObserver.disconnect()
this.resizeObserver = null
},
watch: {
groups () {
// Scroll to top when grid content changes
if (this.$refs.container) {
this.$refs.container.scrollTo(0, 0)
}
},
activeGroup (group) {
this.$emit('activeGroup', group)
}
},
methods: {
onScroll () {
this.scrollPos = this.$refs.container.scrollTop
},
onEmoji (emoji) {
this.$emit('emoji', emoji)
},
scrollToItem (itemId) {
const container = this.$refs.container
if (!container) return
for (const item of this.itemList) {
if (item.id === itemId) {
container.scrollTo(0, item.position.y)
return
}
}
}
},
computed: {
// Total height of scroller content
gridHeight () {
if (this.itemList.length === 0) return 0
const lastItem = this.itemList[this.itemList.length - 1]
return (
lastItem.position.y +
('title' in lastItem ? GROUP_TITLE_HEIGHT : EMOJI_SIZE)
)
},
activeGroup () {
const items = this.itemList
for (let i = items.length - 1; i >= 0; i--) {
const item = items[i]
if ('title' in item && item.position.y <= this.scrollPos) {
return item.id
}
}
return null
},
itemList () {
const items = []
let x = 0
let y = 0
for (const group of this.groups) {
items.push({ position: { x, y }, id: group.id, title: group.text })
if (group.text.length) {
y += GROUP_TITLE_HEIGHT
}
for (const emoji of group.emojis) {
items.push({
position: { x, y },
id: `${group.id}-${emoji.displayText}`,
emoji
})
x += EMOJI_SIZE
if (x + EMOJI_SIZE > this.containerWidth) {
y += EMOJI_SIZE
x = 0
}
}
if (x > 0) {
y += EMOJI_SIZE
x = 0
}
}
return items
},
visibleItems () {
const startPos = this.scrollPos - BUFFER_SIZE
const endPos = this.scrollPos + this.containerHeight + BUFFER_SIZE
return this.itemList.filter((i) => {
return i.position.y >= startPos && i.position.y < endPos
})
},
scrolledClass () {
if (this.scrollPos <= 5) {
return 'scrolled-top'
} else if (this.scrollPos >= this.gridHeight - this.containerHeight - 5) {
return 'scrolled-bottom'
} else {
return 'scrolled-middle'
}
}
}
}
export default EmojiGrid

View file

@ -1,60 +0,0 @@
.emoji {
&-grid {
flex: 1 1 1px;
position: relative;
overflow: auto;
user-select: none;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white);
transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different
-webkit-mask-composite: xor;
mask-composite: exclude;
&.scrolled {
&-top {
mask-size: 100% 20px, 100% 0, auto;
}
&-bottom {
mask-size: 100% 0, 100% 20px, auto;
}
}
margin-left: 5px;
min-height: 200px;
}
&-group-title {
position: absolute;
font-size: 0.85em;
width: 100%;
margin: 0;
height: 24px;
display: flex;
align-items: end;
&.disabled {
display: none;
}
}
&-item {
position: absolute;
width: 32px;
height: 32px;
box-sizing: border-box;
display: flex;
font-size: 32px;
align-items: center;
justify-content: center;
margin: 4px;
cursor: pointer;
img {
object-fit: contain;
max-width: 100%;
max-height: 100%;
}
}
}

View file

@ -1,48 +0,0 @@
<template>
<div
ref="container"
class="emoji-grid"
:class="scrolledClass"
@scroll.passive="onScroll"
>
<div
:style="{
height: `${gridHeight}px`,
}"
>
<template v-for="item in visibleItems">
<h6
v-if="'title' in item && item.title.length"
:key="'title-' + item.id"
class="emoji-group-title"
:style="{
top: item.position.y + 'px',
left: item.position.x + 'px'
}"
>
{{ item.title }}
</h6>
<span
v-else-if="'emoji' in item"
:key="'emoji-' + item.id"
class="emoji-item"
:title="item.emoji.displayText"
:style="{
top: item.position.y + 'px',
left: item.position.x + 'px'
}"
@click.stop.prevent="onEmoji(item.emoji)"
>
<span v-if="!item.emoji.imageUrl">{{ item.emoji.replacement }}</span>
<img
v-else
:src="item.emoji.imageUrl"
>
</span>
</template>
</div>
</div>
</template>
<script src="./emoji_grid.js"></script>
<style lang="scss" src="./emoji_grid.scss"></style>

View file

@ -205,6 +205,7 @@ const EmojiInput = {
},
triggerShowPicker () {
this.showPicker = true
this.$refs.picker.startEmojiLoad()
this.$nextTick(() => {
this.scrollIntoView()
this.focusPickerInput()
@ -222,6 +223,7 @@ const EmojiInput = {
this.showPicker = !this.showPicker
if (this.showPicker) {
this.scrollIntoView()
this.$refs.picker.startEmojiLoad()
this.$nextTick(this.focusPickerInput)
}
},

View file

@ -18,7 +18,6 @@
<EmojiPicker
v-if="enableEmojiPicker"
ref="picker"
show-keep-open
:class="{ hide: !showPicker }"
:enable-sticker-picker="enableStickerPicker"
class="emoji-picker-panel"

View file

@ -1,6 +1,5 @@
import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue'
import EmojiGrid from '../emoji_grid/emoji_grid.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faBoxOpen,
@ -15,17 +14,19 @@ library.add(
faSmileBeam
)
// At widest, approximately 20 emoji are visible in a row,
// loading 3 rows, could be overkill for narrow picker
const LOAD_EMOJI_BY = 60
// When to start loading new batch emoji, in pixels
const LOAD_EMOJI_MARGIN = 64
const EmojiPicker = {
props: {
enableStickerPicker: {
required: false,
type: Boolean,
default: false
},
showKeepOpen: {
required: false,
type: Boolean,
default: false
}
},
data () {
@ -33,13 +34,16 @@ const EmojiPicker = {
keyword: '',
activeGroup: 'standard',
showingStickers: false,
keepOpen: false
groupsScrolledClass: 'scrolled-top',
keepOpen: false,
customEmojiBufferSlice: LOAD_EMOJI_BY,
customEmojiTimeout: null,
customEmojiLoadAllConfirmed: false
}
},
components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
Checkbox,
EmojiGrid
Checkbox
},
methods: {
onStickerUploaded (e) {
@ -52,6 +56,12 @@ const EmojiPicker = {
const value = emoji.imageUrl ? `:${emoji.displayText}:` : emoji.replacement
this.$emit('emoji', { insertion: value, keepOpen: this.keepOpen })
},
onScroll (e) {
const target = (e && e.target) || this.$refs['emoji-groups']
this.updateScrolledClass(target)
this.scrolledGroup(target)
this.triggerLoadMore(target)
},
onWheel (e) {
e.preventDefault()
this.$refs['emoji-tabs'].scrollBy(e.deltaY, 0)
@ -59,12 +69,68 @@ const EmojiPicker = {
highlight (key) {
this.setShowStickers(false)
this.activeGroup = key
if (this.keyword.length) {
this.$refs.emojiGrid.scrollToItem(key)
},
updateScrolledClass (target) {
if (target.scrollTop <= 5) {
this.groupsScrolledClass = 'scrolled-top'
} else if (target.scrollTop >= target.scrollTopMax - 5) {
this.groupsScrolledClass = 'scrolled-bottom'
} else {
this.groupsScrolledClass = 'scrolled-middle'
}
},
onActiveGroup (group) {
this.activeGroup = group
triggerLoadMore (target) {
const ref = this.$refs['group-end-custom']
if (!ref) return
const bottom = ref.offsetTop + ref.offsetHeight
const scrollerBottom = target.scrollTop + target.clientHeight
const scrollerTop = target.scrollTop
const scrollerMax = target.scrollHeight
// Loads more emoji when they come into view
const approachingBottom = bottom - scrollerBottom < LOAD_EMOJI_MARGIN
// Always load when at the very top in case there's no scroll space yet
const atTop = scrollerTop < 5
// Don't load when looking at unicode category or at the very bottom
const bottomAboveViewport = bottom < scrollerTop || scrollerBottom === scrollerMax
if (!bottomAboveViewport && (approachingBottom || atTop)) {
this.loadEmoji()
}
},
scrolledGroup (target) {
const top = target.scrollTop + 5
this.$nextTick(() => {
this.emojisView.forEach(group => {
const ref = this.$refs['group-' + group.id]
if (ref.offsetTop <= top) {
this.activeGroup = group.id
}
})
})
},
loadEmoji () {
const allLoaded = this.customEmojiBuffer.length === this.filteredEmoji.length
if (allLoaded) {
return
}
this.customEmojiBufferSlice += LOAD_EMOJI_BY
},
startEmojiLoad (forceUpdate = false) {
if (!forceUpdate) {
this.keyword = ''
}
this.$nextTick(() => {
this.$refs['emoji-groups'].scrollTop = 0
})
const bufferSize = this.customEmojiBuffer.length
const bufferPrefilledAll = bufferSize === this.filteredEmoji.length
if (bufferPrefilledAll && !forceUpdate) {
return
}
this.customEmojiBufferSlice = LOAD_EMOJI_BY
},
toggleStickers () {
this.showingStickers = !this.showingStickers
@ -80,6 +146,13 @@ const EmojiPicker = {
})
}
},
watch: {
keyword () {
this.customEmojiLoadAllConfirmed = false
this.onScroll()
this.startEmojiLoad(true)
}
},
computed: {
activeGroupView () {
return this.showingStickers ? '' : this.activeGroup
@ -95,6 +168,9 @@ const EmojiPicker = {
this.$store.state.instance.customEmoji || []
)
},
customEmojiBuffer () {
return this.filteredEmoji.slice(0, this.customEmojiBufferSlice)
},
emojis () {
const standardEmojis = this.$store.state.instance.emoji || []
const customEmojis = this.sortedEmoji

View file

@ -1,16 +1,5 @@
@import '../../_variables.scss';
// The worst query selector ever
// selects ONLY emojis pickers in replies in notifications
// who thought this was a good idea?
.notification > .Status > .status-container > .post-status-form > form > .form-group > .emoji-input > .emoji-picker {
max-width: 100%;
left: 0;
@media (min-width: 1300px) {
left: -30px;
}
}
.Notification {
.emoji-picker {
min-width: 160%;
@ -18,7 +7,7 @@
overflow: hidden;
left: -70%;
max-width: 100%;
@media (min-width: 800px) and (max-width: 1280px) {
@media (min-width: 800px) and (max-width: 1300px) {
left: -50%;
min-width: 50%;
max-width: 130%;
@ -29,10 +18,6 @@
min-width: 50%;
max-width: 130%;
}
.Status > .emoji-picker {
z-index: 1000;
}
}
}
.emoji-picker {
@ -85,6 +70,10 @@
flex-grow: 1;
}
.emoji-groups {
min-height: 200px;
}
.additional-tabs {
border-left: 1px solid;
border-left-color: $fallback--icon;
@ -163,12 +152,76 @@
}
}
.emoji-search {
padding: 5px;
flex: 0 0 auto;
.emoji {
&-search {
padding: 5px;
flex: 0 0 auto;
input {
width: 100%;
input {
width: 100%;
}
}
&-groups {
flex: 1 1 1px;
position: relative;
overflow: auto;
user-select: none;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white);
transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different
-webkit-mask-composite: xor;
mask-composite: exclude;
&.scrolled {
&-top {
mask-size: 100% 20px, 100% 0, auto;
}
&-bottom {
mask-size: 100% 0, 100% 20px, auto;
}
}
}
&-group {
display: flex;
align-items: center;
flex-wrap: wrap;
padding-left: 5px;
justify-content: left;
&-title {
font-size: 0.85em;
width: 100%;
margin: 0;
&.disabled {
display: none;
}
}
}
&-item {
width: 32px;
height: 32px;
box-sizing: border-box;
display: flex;
font-size: 32px;
align-items: center;
justify-content: center;
margin: 4px;
cursor: pointer;
img {
object-fit: contain;
max-width: 100%;
max-height: 100%;
}
}
}
}

View file

@ -2,9 +2,9 @@
<div class="emoji-picker panel panel-default panel-body">
<div class="heading">
<span
ref="emoji-tabs"
class="emoji-tabs"
@wheel="onWheel"
ref="emoji-tabs"
>
<span
v-for="group in emojis"
@ -51,16 +51,40 @@
@input="$event.target.composing = false"
>
</div>
<EmojiGrid
ref="emojiGrid"
:groups="emojisView"
@emoji="onEmoji"
@active-group="onActiveGroup"
/>
<div
v-if="showKeepOpen"
class="keep-open"
ref="emoji-groups"
class="emoji-groups"
:class="groupsScrolledClass"
@scroll="onScroll"
>
<div
v-for="group in emojisView"
:key="group.id"
class="emoji-group"
>
<h6
:ref="'group-' + group.id"
class="emoji-group-title"
>
{{ group.text }}
</h6>
<span
v-for="emoji in group.emojis"
:key="group.id + emoji.displayText"
:title="emoji.displayText"
class="emoji-item"
@click.stop.prevent="onEmoji(emoji)"
>
<span v-if="!emoji.imageUrl">{{ emoji.replacement }}</span>
<img
v-else
:src="emoji.imageUrl"
>
</span>
<span :ref="'group-end-' + group.id" />
</div>
</div>
<div class="keep-open">
<Checkbox v-model="keepOpen">
{{ $t('emoji.keep_open') }}
</Checkbox>

View file

@ -144,7 +144,6 @@ const ExtraButtons = {
statusPoll: this.status.poll,
statusFiles: [...this.status.attachments],
statusScope: this.status.visibility,
statusLanguage: this.status.language,
statusContentType: data.content_type
}))
this.doDeleteStatus()

View file

@ -43,7 +43,6 @@ const FollowRequestCard = {
doApprove () {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
this.$store.dispatch('decrementFollowRequestsCount')
const notifId = this.findFollowRequestNotificationId()
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
@ -67,7 +66,6 @@ const FollowRequestCard = {
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
.then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
this.$store.dispatch('decrementFollowRequestsCount')
this.$store.dispatch('removeFollowRequest', this.user)
})
this.hideDenyConfirmDialog()
@ -82,11 +80,6 @@ const FollowRequestCard = {
},
shouldConfirmDeny () {
return this.mergedConfig.modalOnDenyFollow
},
show () {
const notifId = this.$store.state.api.followRequests.find(req => req.id === this.user.id)
return notifId !== undefined
}
}
}

View file

@ -1,5 +1,5 @@
<template>
<basic-user-card :user="user" v-if="show">
<basic-user-card :user="user">
<div class="follow-request-card-content-container">
<button
class="btn button-default"

View file

@ -1,26 +1,10 @@
import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more'
import List from '../list/list.vue'
import get from 'lodash/get'
const FollowRequestList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchFollowRequests'),
select: (props, $store) => get($store.state.api, 'followRequests', []).map(req => $store.getters.findUser(req.id)),
destroy: (props, $store) => $store.dispatch('clearFollowRequests'),
childPropName: 'items',
additionalPropNames: ['userId']
})(List);
const FollowRequests = {
components: {
FollowRequestCard,
FollowRequestList
FollowRequestCard
},
computed: {
userId () {
return this.$store.state.users.currentUser.id
},
requests () {
return this.$store.state.api.followRequests
}

View file

@ -6,11 +6,12 @@
</div>
</div>
<div class="panel-body">
<FollowRequestList :user-id="userId">
<template #item="{item}">
<FollowRequestCard :user="item" />
</template>
</FollowRequestList>
<FollowRequestCard
v-for="request in requests"
:key="request.id"
:user="request"
class="list-item"
/>
</div>
</div>
</template>

View file

@ -1,77 +0,0 @@
<template>
<div class="followed-tag-card">
<span>
<router-link :to="{ name: 'tag-timeline', params: {tag: tag.name}}">
<span class="tag-link">#{{ tag.name }}</span>
</router-link>
<span class="unfollow-tag">
<button
v-if="isFollowing"
class="button-default unfollow-tag-button"
:title="$t('user_card.unfollow_tag')"
@click="unfollowTag(tag.name)"
>
{{ $t('user_card.unfollow_tag') }}
</button>
<button
v-else
class="button-default follow-tag-button"
:title="$t('user_card.follow_tag')"
@click="followTag(tag.name)"
>
{{ $t('user_card.follow_tag') }}
</button>
</span>
</span>
</div>
</template>
<script>
export default {
name: 'FollowedTagCard',
props: {
tag: {
type: Object,
required: true
},
},
// this is a hack to update the state of the button
// for some reason, List does not update on changes to the tag object
data: () => ({
isFollowing: true
}),
mounted () {
this.isFollowing = this.tag.following
},
methods: {
unfollowTag (tag) {
this.$store.dispatch('unfollowTag', tag)
this.isFollowing = false
},
followTag (tag) {
this.$store.dispatch('followTag', tag)
this.isFollowing = true
}
}
}
</script>
<style scoped>
.followed-tag-card {
margin-left: 1rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.unfollow-tag {
position: absolute;
right: 1rem;
}
.tag-link {
font-size: large;
}
.unfollow-tag-button, .follow-tag-button {
font-size: medium;
}
</style>

View file

@ -33,6 +33,11 @@ library.add(
)
const NavPanel = {
created () {
if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequests')
}
},
components: {
TimelineMenuContent
},
@ -49,13 +54,11 @@ const NavPanel = {
computed: {
...mapState({
currentUser: state => state.users.currentUser,
followRequestCount: state => state.api.followRequests.length,
privateMode: state => state.instance.private,
federating: state => state.instance.federating
}),
...mapGetters(['unreadAnnouncementCount']),
followRequestCount () {
return this.$store.state.users.currentUser.follow_requests_count
}
...mapGetters(['unreadAnnouncementCount'])
}
}

View file

@ -1,4 +1,4 @@
import PinchZoom from '@floatingghost/pinch-zoom-element'
import PinchZoom from '@kazvmoe-infra/pinch-zoom-element'
export default {
methods: {

View file

@ -13,7 +13,6 @@ import suggestor from '../emoji_input/suggestor.js'
import { mapGetters, mapState } from 'vuex'
import Checkbox from '../checkbox/checkbox.vue'
import Select from '../select/select.vue'
import iso6391 from 'iso-639-1'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -64,7 +63,6 @@ const PostStatusForm = {
'statusMediaDescriptions',
'statusScope',
'statusContentType',
'statusLanguage',
'replyTo',
'quoteId',
'repliedUser',
@ -130,7 +128,7 @@ const PostStatusForm = {
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
}
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage } = this.$store.getters.mergedConfig
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject } = this.$store.getters.mergedConfig
let statusParams = {
spoilerText: this.subject || '',
@ -141,7 +139,6 @@ const PostStatusForm = {
poll: {},
mediaDescriptions: {},
visibility: this.suggestedVisibility(),
language: interfaceLanguage,
contentType
}
@ -156,7 +153,6 @@ const PostStatusForm = {
poll: this.statusPoll || {},
mediaDescriptions: this.statusMediaDescriptions || {},
visibility: this.statusScope || this.suggestedVisibility(),
language: this.statusLanguage || interfaceLanguage,
contentType: statusContentType
}
}
@ -263,10 +259,7 @@ const PostStatusForm = {
...mapGetters(['mergedConfig']),
...mapState({
mobileLayout: state => state.interface.mobileLayout
}),
isoLanguages () {
return iso6391.getAllCodes();
}
})
},
watch: {
'newStatus': {
@ -289,7 +282,6 @@ const PostStatusForm = {
files: [],
visibility: newStatus.visibility,
contentType: newStatus.contentType,
language: newStatus.language,
poll: {},
mediaDescriptions: {}
}
@ -349,7 +341,6 @@ const PostStatusForm = {
inReplyToStatusId: this.replyTo,
quoteId: this.quoteId,
contentType: newStatus.contentType,
language: newStatus.language,
poll,
idempotencyKey: this.idempotencyKey
}
@ -384,7 +375,6 @@ const PostStatusForm = {
inReplyToStatusId: this.replyTo,
quoteId: this.quoteId,
contentType: newStatus.contentType,
language: newStatus.language,
poll: {},
preview: true
}).then((data) => {

View file

@ -194,23 +194,6 @@
:on-scope-change="changeVis"
/>
<div
class="language-selector"
>
<Select
id="post-language"
v-model="newStatus.language"
class="form-control"
>
<option
v-for="language in isoLanguages"
:key="language"
:value="language"
>
{{ language }}
</option>
</Select>
</div>
<div
v-if="postFormats.length > 1"
class="text-format"

View file

@ -16,6 +16,7 @@
class="fa-scale-110 fa-old-padding"
/>
</button>
{{ ' ' }}
<button
v-if="showPrivate"
class="button-unstyled scope"
@ -29,6 +30,7 @@
class="fa-scale-110 fa-old-padding"
/>
</button>
{{ ' ' }}
<button
v-if="showUnlisted"
class="button-unstyled scope"
@ -42,6 +44,7 @@
class="fa-scale-110 fa-old-padding"
/>
</button>
{{ ' ' }}
<button
v-if="showPublic"
class="button-unstyled scope"
@ -84,7 +87,6 @@
min-width: 1.3em;
min-height: 1.3em;
text-align: center;
margin-right: 0.4em;
&.selected svg {
color: $fallback--lightText;

View file

@ -73,7 +73,6 @@
.search-bar-input {
flex: 1 0 auto;
margin-left: 0.5em;
}
.cancel-search {

View file

@ -76,10 +76,6 @@
position: absolute;
right: 20px;
padding-right: 10px;
@media all and (max-width: 800px) {
display: none;
}
}
}
}

View file

@ -44,10 +44,6 @@
<div class="panel-body">
<SettingsModalContent v-if="modalOpenedOnce" />
</div>
<span
id="unscrolled-content"
class="extra-content"
/>
<div class="panel-footer settings-footer">
<Popover
class="export"
@ -57,7 +53,7 @@
:bound-to="{ x: 'container' }"
remove-padding
>
<template #trigger>
<template v-slot:trigger>
<button
class="btn button-default"
:title="$t('general.close')"
@ -69,7 +65,7 @@
/>
</button>
</template>
<template #content="{close}">
<template v-slot:content="{close}">
<div class="dropdown-menu">
<button
class="button-default dropdown-item dropdown-item-icon"
@ -107,11 +103,14 @@
<Checkbox
:model-value="!!expertLevel"
class="expertMode"
@update:modelValue="expertLevel = Number($event)"
>
{{ $t("settings.expert_mode") }}
</Checkbox>
<span
id="unscrolled-content"
class="extra-content"
/>
<button
v-if="currentUser"
class="button-default logout-button"

View file

@ -407,15 +407,6 @@
{{ $t('settings.preload_images') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="useBlurhash"
expert="1"
:disabled="!hideNsfw"
>
{{ $t('settings.use_blurhash') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="useOneClickNsfw"

View file

@ -43,9 +43,7 @@ const ProfileTab = {
bannerPreview: null,
background: null,
backgroundPreview: null,
emailLanguage: this.$store.state.users.currentUser.language || '',
newPostTTLDays: this.$store.state.users.currentUser.status_ttl_days,
expirePosts: this.$store.state.users.currentUser.status_ttl_days !== null,
emailLanguage: this.$store.state.users.currentUser.language || ''
}
},
components: {
@ -125,8 +123,7 @@ const ProfileTab = {
display_name: this.newName,
fields_attributes: this.newFields.filter(el => el != null),
bot: this.bot,
show_role: this.showRole,
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1
show_role: this.showRole
/* eslint-enable camelcase */
}

View file

@ -4,10 +4,6 @@
margin: 0;
}
.expire-posts-days {
margin-left: 1em;
}
.visibility-tray {
padding-top: 5px;
}

View file

@ -89,20 +89,6 @@
{{ $t('settings.bot') }}
</Checkbox>
</p>
<p>
<Checkbox v-model="expirePosts">
{{ $t('settings.expire_posts_enabled') }}
</Checkbox>
<input
v-model="newPostTTLDays"
:disabled="!expirePosts"
type="number"
min="1"
max="730"
class="expire-posts-days"
:placeholder="$t('settings.expire_posts_input_placeholder')"
/>
</p>
<p>
<interface-language-switcher
:prompt-text="$t('settings.email_language')"

View file

@ -284,6 +284,7 @@
box-shadow: none;
background: transparent;
color: var(--faint, $fallback--faint);
align-self: stretch;
}
.theme-color-cl,
@ -317,11 +318,11 @@
.extra-content {
.apply-container {
padding-left: 15vw;
display: flex;
flex-direction: row;
justify-content: space-evenly;
justify-content: space-around;
flex-grow: 1;
.btn {
flex-grow: 1;
min-height: 2em;

View file

@ -958,22 +958,20 @@
v-if="isActive"
to="#unscrolled-content"
>
<div class="panel-body settings-footer">
<div class="apply-container">
<button
class="btn button-default submit"
:disabled="!themeValid"
@click="setCustomTheme"
>
{{ $t('general.apply') }}
</button>
<button
class="btn button-default"
@click="clearAll"
>
{{ $t('settings.style.switcher.reset') }}
</button>
</div>
<div class="apply-container">
<button
class="btn button-default submit"
:disabled="!themeValid"
@click="setCustomTheme"
>
{{ $t('general.apply') }}
</button>
<button
class="btn button-default"
@click="clearAll"
>
{{ $t('settings.style.switcher.reset') }}
</button>
</div>
</teleport>
</div>

View file

@ -42,10 +42,6 @@
display: flex;
padding: var(--status-margin, $status-margin);
.content {
overflow: hidden;
}
> * {
min-width: 0;
}

View file

@ -352,25 +352,22 @@
</div>
</div>
<div class="content">
<StatusContent
ref="content"
class="status-content"
:status="status"
:no-heading="noHeading"
:highlight="highlight"
:focused="isFocused"
:controlled-showing-tall="controlledShowingTall"
:controlled-expanding-subject="controlledExpandingSubject"
:controlled-showing-long-subject="controlledShowingLongSubject"
:controlled-toggle-showing-tall="controlledToggleShowingTall"
:controlled-toggle-expanding-subject="controlledToggleExpandingSubject"
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
@mediaplay="addMediaPlaying($event)"
@mediapause="removeMediaPlaying($event)"
@parseReady="setHeadTailLinks"
/>
</div>
<StatusContent
ref="content"
:status="status"
:no-heading="noHeading"
:highlight="highlight"
:focused="isFocused"
:controlled-showing-tall="controlledShowingTall"
:controlled-expanding-subject="controlledExpandingSubject"
:controlled-showing-long-subject="controlledShowingLongSubject"
:controlled-toggle-showing-tall="controlledToggleShowingTall"
:controlled-toggle-expanding-subject="controlledToggleExpandingSubject"
:controlled-toggle-showing-long-subject="controlledToggleShowingLongSubject"
@mediaplay="addMediaPlaying($event)"
@mediapause="removeMediaPlaying($event)"
@parseReady="setHeadTailLinks"
/>
<div
v-if="inConversation && !isPreview && replies && replies.length"
@ -537,6 +534,6 @@
</div>
</template>
<script src="./status.js"></script>
<script src="./status.js" ></script>
<style src="./status.scss" lang="scss"></style>

View file

@ -68,6 +68,7 @@
.StatusContent {
flex: 1;
min-width: 0;
overflow: hidden;
img, video {
&.emoji {

View file

@ -6,13 +6,11 @@ import TimelineMenuTabs from '../timeline_menu_tabs/timeline_menu_tabs.vue'
import TimelineQuickSettings from './timeline_quick_settings.vue'
import { debounce, throttle, keyBy } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch, faCog, faPlus, faMinus } from '@fortawesome/free-solid-svg-icons'
import { faCircleNotch, faCog } from '@fortawesome/free-solid-svg-icons'
library.add(
faCircleNotch,
faCog,
faPlus,
faMinus
faCog
)
const Timeline = {
@ -92,15 +90,6 @@ const Timeline = {
},
showPanelNavShortcuts () {
return this.$store.getters.mergedConfig.showPanelNavShortcuts
},
currentUser () {
return this.$store.state.users.currentUser
},
tagData () {
return this.$store.state.tags.tags[this.tag]
},
tagFollowed () {
return this.$store.state.tags.tags[this.tag]?.following
}
},
created () {
@ -129,10 +118,6 @@ const Timeline = {
}
window.addEventListener('keydown', this.handleShortKey)
setTimeout(this.determineVisibleStatuses, 250)
if (this.tag) {
this.$store.dispatch('getTag', this.tag)
}
},
unmounted () {
window.removeEventListener('scroll', this.handleScroll)
@ -247,12 +232,6 @@ const Timeline = {
}, 200),
handleVisibilityChange () {
this.unfocused = document.hidden
},
followTag (tag) {
return this.$store.dispatch('followTag', tag)
},
unfollowTag (tag) {
return this.$store.dispatch('unfollowTag', tag)
}
},
watch: {

View file

@ -21,36 +21,6 @@
{{ $t('timeline.up_to_date') }}
</div>
<TimelineQuickSettings v-if="!embedded" />
<div
v-if="currentUser && tag !== undefined && tagData && !tagFollowed"
class="followTag"
>
<button
class="button-default"
:title="$t('timeline.follow_tag')"
@click="followTag(tag)"
>
<FAIcon
size="sm"
icon="plus"
/>
</button>
</div>
<div
v-if="currentUser && tag !== undefined && tagData && tagFollowed"
class="followTag"
>
<button
class="button-default"
:title="$t('timeline.unfollow_tag')"
@click="unfollowTag(tag)"
>
<FAIcon
size="sm"
icon="minus"
/>
</button>
</div>
</div>
<div :class="classes.body">
<div

View file

@ -2,7 +2,7 @@
.user-card {
position: relative;
z-index: 10;
z-index: 1;
&:hover {
--_still-image-img-visibility: visible;
@ -235,7 +235,7 @@
line-height: 22px;
flex-wrap: wrap;
.following, .requested_by, .blocking {
.following, .requested_by {
flex: 1 0 auto;
margin: 0;
margin-bottom: .25em;

View file

@ -67,17 +67,6 @@
icon="external-link-alt"
/>
</a>
<a
v-if="isOtherUser"
:href="user.statusnet_profile_url + '.rss'"
target="_blank"
class="button-unstyled external-link-button"
>
<FAIcon
class="icon"
icon="rss"
/>
</a>
<AccountActions
v-if="isOtherUser && loggedIn"
:user="user"
@ -127,12 +116,6 @@
</div>
</div>
<div class="user-meta">
<div
v-if="relationship.blocked_by && loggedIn && isOtherUser"
class="blocking"
>
{{ $t('user_card.blocks_you') }}
</div>
<div
v-if="relationship.followed_by && loggedIn && isOtherUser"
class="following"
@ -193,7 +176,6 @@
<FollowButton
:relationship="relationship"
:user="user"
:disabled="relationship.blocked_by"
/>
<template v-if="relationship.following">
<ProgressButton

View file

@ -10,14 +10,11 @@ import withLoadMore from '../../hocs/with_load_more/with_load_more'
import { debounce } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faCircleNotch,
faCircleCheck
faCircleNotch
} from '@fortawesome/free-solid-svg-icons'
import FollowedTagCard from '../followed_tag_card/FollowedTagCard.vue'
library.add(
faCircleNotch,
faCircleCheck
faCircleNotch
)
const FollowerList = withLoadMore({
@ -36,14 +33,6 @@ const FriendList = withLoadMore({
additionalPropNames: ['userId']
})(List)
const FollowedTagList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchFollowedTags', props.userId),
select: (props, $store) => get($store.getters.findUser(props.userId), 'followedTagIds', []).map(id => $store.getters.findTag(id)),
destroy: (props, $store) => $store.dispatch('clearFollowedTags', props.userId),
childPropName: 'items',
additionalPropNames: ['userId']
})(List)
const isUserPage = ({ name }) => name === 'user-profile' || name === 'external-user-profile'
const UserProfile = {
@ -52,7 +41,6 @@ const UserProfile = {
error: false,
userId: null,
tab: 'statuses',
followsTab: 'users',
footerRef: null,
note: null,
noteLoading: false
@ -177,9 +165,6 @@ const UserProfile = {
this.tab = tab
this.$router.replace({ hash: `#${tab}` })
},
onFollowsTabSwitch (tab) {
this.followsTab = tab
},
linkClicked ({ target }) {
if (target.tagName === 'SPAN') {
target = target.parentNode
@ -215,7 +200,6 @@ const UserProfile = {
}
},
components: {
FollowedTagCard,
UserCard,
Timeline,
FollowerList,
@ -223,8 +207,7 @@ const UserProfile = {
FollowCard,
TabSwitcher,
Conversation,
RichContent,
FollowedTagList
RichContent
}
}

View file

@ -37,15 +37,6 @@
:html="field.value"
:emoji="user.emoji"
/>
<span
v-if="field.verified_at"
class="user-profile-field-validated"
>
<FAIcon
icon="check-circle"
:title="$t('user_profile.field_validated')"
/>
</span>
</dd>
</dl>
</div>
@ -104,48 +95,22 @@
v-if="followsTabVisible"
key="followees"
:label="$t('user_card.followees')"
:disabled="!user.friends_count"
>
<tab-switcher
:active-tab="followsTab"
:render-only-focused="true"
:on-switch="onFollowsTabSwitch"
>
<div
key="users"
:label="$t('user_card.followed_users')"
>
<FriendList :user-id="userId">
<template #item="{item}">
<FollowCard :user="item" />
</template>
</FriendList>
</div>
<div
key="tags"
v-if="isUs"
:label="$t('user_card.followed_tags')"
>
<FollowedTagList
:user-id="userId"
:get-key="(item) => item.name"
>
<template #item="{item}">
<FollowedTagCard :tag="item" />
</template>
<template #empty>
{{ $t('user_card.not_following_any_hashtags')}}
</template>
</FollowedTagList>
</div>
</tab-switcher>
<FriendList :user-id="userId">
<template v-slot:item="{item}">
<FollowCard :user="item" />
</template>
</FriendList>
</div>
<div
v-if="followersTabVisible"
key="followers"
:label="$t('user_card.followers')"
:disabled="!user.followers_count"
>
<FollowerList :user-id="userId">
<template #item="{item}">
<template v-slot:item="{item}">
<FollowCard
:user="item"
:no-follows-you="isUs"
@ -260,11 +225,6 @@
padding: 0.5em 1.5em;
box-sizing: border-box;
}
.user-profile-field-validated {
margin-left: 1rem;
color: green;
}
}
}

View file

@ -59,8 +59,7 @@ const withLoadMore = ({
this.loading = false
this.bottomedOut = isEmpty(newEntries)
})
.catch((e) => {
console.error(e)
.catch(() => {
this.loading = false
this.error = true
})
@ -89,7 +88,7 @@ const withLoadMore = ({
const children = this.$slots
return (
<div class="with-load-more">
<WrappedComponent {...props} >
<WrappedComponent {...props}>
{children}
</WrappedComponent>
<div class="with-load-more-footer">

17
src/i18n.js Normal file
View file

@ -0,0 +1,17 @@
import 'custom-event-polyfill'
import './lib/event_target_polyfill.js'
import { createI18n } from 'vue-i18n'
import messages from './i18n/messages.js'
const currentLocale = (window.navigator.language || 'en').split('-')[0]
const i18n = createI18n({
// By default, use the browser locale, we will update it if neccessary
locale: 'en',
fallbackLocale: 'en',
messages: messages.default
})
messages.setLanguage(i18n, currentLocale)
export default i18n;

View file

@ -135,7 +135,7 @@
},
"scope_in_timeline": {
"direct": "Direkt",
"local": "Lokal - nur deine eigene Instanz kann diese Nachricht sehen",
"local": "Lokal - nur deine eigene Instanz kann diesen Beitrag sehen",
"private": "Nur an Folgende",
"public": "Öffentlich",
"unlisted": "Nicht gelistet"
@ -345,10 +345,10 @@
},
"content_warning": "Inhaltswarnung (optional)",
"default": "Sitze gerade im Hofbräuhaus",
"direct_warning_to_all": "Diese Nachricht wird für alle erwähnten Benutzer sichtbar sein.",
"direct_warning_to_first_only": "Diese Nachricht wird für alle Benutzer, die am Anfang der Nachricht erwähnt wurden, sichtbar sein.",
"direct_warning_to_all": "Dieser Beitrag wird für alle erwähnten Benutzer sichtbar sein.",
"direct_warning_to_first_only": "Dieser Beitrag wird für alle Benutzer, die am Anfang der Nachricht erwähnt wurden, sichtbar sein.",
"edit_remote_warning": "Änderungen könnten auf manchen Instanzen nicht sichtbar sein!",
"edit_status": "Nachricht bearbeiten",
"edit_status": "Beitrag ändern",
"edit_unsupported_warning": "Umfragen und Erwähnungen werden durch die Bearbeitung nicht geändert.",
"empty_status_error": "Eine Nachricht ohne Text und ohne Anhänge kann nicht gesendet werden",
"media_description": "Medienbeschreibung",
@ -360,17 +360,17 @@
"preview": "Vorschau",
"preview_empty": "Leer",
"scope": {
"direct": "Direkt - Nachricht nur an erwähnte Profile",
"local": "Lokal - diese Nachricht nicht föderieren",
"private": "Nur Follower - Nachricht nur für Follower sichtbar",
"public": "Öffentlich - Nachricht an öffentliche Zeitleisten",
"direct": "Direkt - Beitrag nur an erwähnte Profile",
"local": "Lokal - diesen Beitrag nicht föderieren",
"private": "Nur Follower - Beitrag nur für Follower sichtbar",
"public": "Öffentlich - Beitrag an öffentliche Zeitleisten",
"unlisted": "Nicht gelistet - Nicht in öffentlichen Zeitleisten anzeigen"
},
"scope_notice": {
"local": "Dieser Bericht ist auf anderen Instanzen nicht sichbar",
"private": "Diese Nachricht wird nur für deine Follower sichtbar sein",
"public": "Diese Nachricht wird für alle sichtbar sein",
"unlisted": "Diese Nachricht wird weder in der öffentlichen Zeitleiste noch im gesamten bekannten Netzwerk sichtbar sein"
"private": "Dieser Beitrag wird nur für deine Follower sichtbar sein",
"public": "Dieser Beitrag wird für alle sichtbar sein",
"unlisted": "Dieser Beitrag wird weder in der öffentlichen Zeitleiste noch im gesamten bekannten Netzwerk sichtbar sein"
}
},
"registration": {
@ -465,10 +465,10 @@
"confirm_dialogs": "Bestätigung erforderlich für:",
"confirm_dialogs_approve_follow": "Annehmen einer Followanfrage",
"confirm_dialogs_block": "Jemanden blockieren",
"confirm_dialogs_delete": "Löschen einer Nachricht",
"confirm_dialogs_delete": "Löschen eines Beitrages",
"confirm_dialogs_deny_follow": "Ablehnen einer Followanfrage",
"confirm_dialogs_mute": "Jemanden stummschalten",
"confirm_dialogs_repeat": "Wiederholen einer Nachricht",
"confirm_dialogs_repeat": "Wiederholen eines Beitrages",
"confirm_dialogs_unfollow": "Folgen beenden",
"confirm_new_password": "Neues Passwort bestätigen",
"confirmation_dialogs": "Bestätigungs-Einstellungen",
@ -535,7 +535,7 @@
"hide_media_previews": "Verstecke Vorschau von Medien",
"hide_muted_posts": "Verberge Beiträge stummgeschalteter Nutzer",
"hide_muted_threads": "Stummgeschaltete Unterhaltungen ausblenden",
"hide_post_stats": "Nachrichtenstatistiken verbergen (z.B. die Anzahl der Favoriten)",
"hide_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)",
"hide_shoutbox": "Shoutbox der Instanz verbergen",
"hide_site_favicon": "Favicon der Instanz im Top-Panel nicht anzeigen",
"hide_site_name": "Instanznamen im Top-Panel nicht anzeigen",
@ -562,7 +562,7 @@
"loop_video_silent_only": "Nur Videos ohne Ton wiederholen (z.B. Mastodons \"gifs\")",
"mascot": "Mastodon-FE-Maskottchen",
"max_depth_in_thread": "Maximale Tiefe, bis zu der Unterhaltungen standardmäßig angezeigt werden",
"max_thumbnails": "Maximale Anzahl von Vorschaubildern pro Nachricht (leer = keine Beschränkung)",
"max_thumbnails": "Maximale Anzahl von Vorschaubildern pro Beitrag",
"mention_link_bolden_you": "eigene Erwähnungen hervorheben",
"mention_link_display": "Erwähungs-Links anzeigen",
"mention_link_display_full": "immer als vollständige Namen (z. B. {'@'}foo{'@'}example.org)",
@ -638,7 +638,7 @@
"panelRadius": "Panel",
"pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist",
"play_videos_in_modal": "Videos in größerem Medienfenster abspielen",
"post_status_content_type": "Standard-Format für Nachrichten",
"post_status_content_type": "Standard-Beitragsart",
"posts": "Beiträge",
"preload_images": "Bilder vorausladen",
"presets": "Voreinstellungen",
@ -656,7 +656,7 @@
"remove_alias": "Dieses Pseudonym entfernen",
"remove_backup": "Entfernen",
"render_mfm": "Misskey-Markdown darstellen",
"render_mfm_on_hover": "MFM-Animationen pausieren, solange sich der Mauszeiger nicht über der Nachricht befindet",
"render_mfm_on_hover": "MFM-Animationen pausieren, solange sich der Mauszeiger nicht über dem Beitrag befindet",
"replies_in_timeline": "Antworten in der Zeitleiste",
"reply_visibility_all": "Alle Antworten zeigen",
"reply_visibility_following": "Zeige nur Antworten an mich oder an Benutzer, denen ich folge",
@ -680,7 +680,7 @@
"security": "Sicherheit",
"security_tab": "Sicherheit",
"sensitive_by_default": "Alle Beiträge standardmäßig als heikel markieren",
"sensitive_if_subject": "Bilder automatisch als heikel markieren, wenn die Nachricht eine Inhaltswarnung hat",
"sensitive_if_subject": "Bilder automatisch als heikel markieren, wenn der Beitrag eine Inhaltswarnung hat",
"set_new_avatar": "Setze einen neuen Avatar",
"set_new_mascot": "Neues Maskottchen einstellen",
"set_new_profile_background": "Setze einen neuen Hintergrund für dein Profil",
@ -758,8 +758,8 @@
"components": {
"input": "Eingabefelder",
"interface": "Oberfläche",
"post": "Nachrichtentext",
"postCode": "nichtproportionaler Text in einer Nachricht (Rich-Text)"
"post": "Beitragstext",
"postCode": "Dicktengleicher Text in einem Beitrag (Rich-Text)"
},
"custom": "Benutzerdefiniert",
"family": "Schriftname",
@ -790,7 +790,7 @@
"component": "Komponente",
"components": {
"avatar": "Benutzer-Avatar (in der Profilansicht)",
"avatarStatus": "Benutzer-Avatar (in der Nachrichtenanzeige)",
"avatarStatus": "Benutzer-Avatar (in der Beitragsanzeige)",
"button": "Schaltfläche",
"buttonHover": "Schaltfläche (hover)",
"buttonPressed": "Schaltfläche (gedrückt)",
@ -861,7 +861,7 @@
"tooltipRadius": "Tooltips/Warnungen",
"translation_language": "Sprache für automatische Übersetzungen",
"tree_advanced": "Weitere Knöpfe zum Öffnen und Schließen von Antworten anzeigen",
"tree_fade_ancestors": "Vorgänger der aktuellen Nachricht schwach darstellen",
"tree_fade_ancestors": "Vorgänger des aktuellen Beitrags schwach darstellen",
"type_domains_to_mute": "Tippe die Domains ein, die du stummschalten willst",
"upload_a_photo": "Lade ein Foto hoch",
"useStreamingApi": "Empfange Posts und Benachrichtigungen in Echtzeit",
@ -888,14 +888,14 @@
"wordfilter": "Wortfilter"
},
"status": {
"ancestor_follow": "Zeige {numReplies} andere Antwort unter dieser Nachricht | Zeige {numReplies} andere Antworten unter dieser Nachricht",
"ancestor_follow": "Zeige {numReplies} andere Antwort unter diesem Beitrag | Zeige {numReplies} andere Antworten unter diesem Beitrag",
"ancestor_follow_with_icon": "{icon} {text}",
"attachment_stop_flash": "Flash-Player stoppen",
"bookmark": "Lesezeichen setzen",
"collapse_attachments": "Anhänge einklappen",
"copy_link": "Link zur Nachricht kopieren",
"delete": "Lösche Nachricht",
"delete_confirm": "Möchtest du diese Nachricht wirklich löschen?",
"copy_link": "Beitragslink kopieren",
"delete": "Lösche Beitrag",
"delete_confirm": "Möchtest du diese Beitrag wirklich löschen?",
"delete_confirm_accept_button": "Ja, löschen",
"delete_confirm_cancel_button": "Nein, behalten",
"delete_confirm_title": "Löschen bestätigen",
@ -909,7 +909,7 @@
"hide_attachment": "Anhänge verbergen",
"hide_content": "Inhalt verbergen",
"hide_full_subject": "Vollständige Inhaltswarnung verbergen",
"many_attachments": "Nachricht hat {number} Anhang | Nachricht hat {number} Anhänge",
"many_attachments": "Beitrag hat {number} Anhang | Beitrag hat {number} Anhänge",
"mentions": "Erwähnungen",
"move_down": "Anhang nach rechts verschieben",
"move_up": "Anhang nach links verschieben",
@ -921,7 +921,7 @@
"pinned": "Angeheftet",
"plus_more": "+{number} mehr",
"remove_attachment": "Anhang entfernen",
"repeat_confirm": "Nachricht wirklich wiederholen?",
"repeat_confirm": "Beitrag wirklich wiederholen?",
"repeat_confirm_accept_button": "Ja, wiederholen",
"repeat_confirm_cancel_button": "Nein, nicht wiederholen",
"repeat_confirm_title": "Wiederholen bestätigen",
@ -930,15 +930,15 @@
"replies_list_with_others": "Zeige noch {numReplies} Antwort | Zeige noch {numReplies} Antworten",
"reply_to": "Antworten auf",
"show_all_attachments": "Alle Anhänge anzeigen",
"show_all_conversation": "Ganzes Gespräch anzeigen (noch {numStatus} Nachricht) | Ganzes Gespräch anzeigen (noch {numStatus} Nachrichten)",
"show_all_conversation": "Ganzes Gespräch anzeigen (noch {numStatus} Beitrag) | Ganzes Gespräch anzeigen (noch {numStatus} Beiträge)",
"show_all_conversation_with_icon": "{icon} {text}",
"show_attachment_description": "Vorschau-Beschreibung (Anhang öffnen für vollständige Beschreibung)",
"show_attachment_in_modal": "Anhang in einem Fenster anzeigen",
"show_content": "Inhalt anzeigen",
"show_full_subject": "Vollständige Inhaltswarnung anzeigen",
"show_only_conversation_under_this": "Nur Antworten auf diesen Bericht anzeigen",
"status_deleted": "Diese Nachricht wurde gelöscht",
"status_unavailable": "Nachricht nicht verfügbar",
"status_deleted": "Dieser Beitrag wurde gelöscht",
"status_unavailable": "Beitrag nicht verfügbar",
"thread_follow": "Zeige noch {numStatus} Antwort | Zeige noch {numStatus} Antworten",
"thread_follow_with_icon": "{icon} {text}",
"thread_hide": "Diese Unterhaltung stummschalten",
@ -979,10 +979,10 @@
"collapse": "Einklappen",
"conversation": "Unterhaltung",
"error": "Fehler beim Lesen der Timeline: {0}",
"load_older": "Lade ältere Nachrichten",
"no_more_statuses": "Keine weiteren Nachrichten",
"no_retweet_hint": "Die Nachricht ist als nur-für-Follower oder Direktnachricht markiert und kann nicht wiederholt oder zitiert werden",
"no_statuses": "Keine Nachrichten",
"load_older": "Lade ältere Beiträge",
"no_more_statuses": "Keine weiteren Beiträge",
"no_retweet_hint": "Der Beitrag ist als nur-für-Follower oder Direktnachricht markiert und kann nicht wiederholt oder zitiert werden",
"no_statuses": "Keine Beiträge",
"reload": "Neu laden",
"repeated": "wiederholte",
"show_new": "Zeige Neuere",
@ -1094,7 +1094,7 @@
"replies": "Mit Antworten",
"report": "Melden",
"show_repeats": "Geteilte Beiträge anzeigen",
"statuses": "Nachrichten",
"statuses": "Beiträge",
"subscribe": "Folgen",
"unblock": "Entblocken",
"unblock_progress": "Entblocken…",

View file

@ -165,72 +165,72 @@
"moves": "User migrates"
},
"languages": {
"bg": "Bulgarian",
"en": "English",
"ar": "Arabic",
"az": "Azerbaijani",
"bg": "Bulgarian",
"zh": "Chinese",
"cs": "Czech",
"da": "Danish",
"de": "German",
"el": "Greek",
"en": "English",
"nl": "Dutch",
"eo": "Esperanto",
"es": "Spanish",
"fa": "Persian",
"fi": "Finnish",
"fr": "French",
"ga": "Irish",
"de": "German",
"el": "Greek",
"he": "Hebrew",
"hi": "Hindi",
"hu": "Hungarian",
"id": "Indonesian",
"ga": "Irish",
"it": "Italian",
"ja": "Japanese",
"ko": "Korean",
"lt": "Lithuanian",
"lv": "Latvian",
"nl": "Dutch",
"fa": "Persian",
"pl": "Polish",
"pt": "Portuguese",
"ru": "Russian",
"sk": "Slovak",
"es": "Spanish",
"sv": "Swedish",
"tr": "Turkish",
"uk": "Ukrainian",
"lt": "Lithuanian",
"lv": "Latvian",
"translated_from": {
"bg": "Translated from @:languages.bg",
"en": "Translated from @:languages.en",
"ar": "Translated from @:languages.ar",
"az": "Translated from @:languages.az",
"bg": "Translated from @:languages.bg",
"zh": "Translated from @:languages.zh",
"cs": "Translated from @:languages.cs",
"da": "Translated from @:languages.da",
"de": "Translated from @:languages.de",
"el": "Translated from @:languages.el",
"en": "Translated from @:languages.en",
"nl": "Translated from @:languages.nl",
"eo": "Translated from @:languages.eo",
"es": "Translated from @:languages.es",
"fa": "Translated from @:languages.fa",
"fi": "Translated from @:languages.fi",
"fr": "Translated from @:languages.fr",
"ga": "Translated from @:languages.ga",
"de": "Translated from @:languages.de",
"el": "Translated from @:languages.el",
"he": "Translated from @:languages.he",
"hi": "Translated from @:languages.hi",
"hu": "Translated from @:languages.hu",
"id": "Translated from @:languages.id",
"ga": "Translated from @:languages.ga",
"it": "Translated from @:languages.it",
"ja": "Translated from @:languages.ja",
"ko": "Translated from @:languages.ko",
"lt": "Translated from @:languages.lt",
"lv": "Translated from @:languages.lv",
"nl": "Translated from @:languages.nl",
"fa": "Translated from @:languages.fa",
"pl": "Translated from @:languages.pl",
"pt": "Translated from @:languages.pt",
"ru": "Translated from @:languages.ru",
"sk": "Translated from @:languages.sk",
"es": "Translated from @:languages.es",
"sv": "Translated from @:languages.sv",
"tr": "Translated from @:languages.tr",
"uk": "Translated from @:languages.uk",
"zh": "Translated from @:languages.zh"
},
"uk": "Ukrainian",
"zh": "Chinese"
"lt": "Translated from @:languages.lt",
"lv": "Translated from @:languages.lv"
}
},
"lists": {
"create": "Create",
@ -254,15 +254,15 @@
"hint": "Log in to join the discussion",
"login": "Log in",
"logout": "Log out",
"logout_confirm": "Are you sure you want to log out?",
"logout_confirm_accept_button": "Log out",
"logout_confirm_cancel_button": "Cancel",
"logout_confirm_title": "Log out",
"password": "Password",
"placeholder": "myusername",
"recovery_code": "Recovery code",
"register": "Register",
"username": "Username"
"username": "Username",
"logout_confirm_cancel_button": "Cancel",
"logout_confirm_accept_button": "Log out",
"logout_confirm": "Are you sure you want to log out?",
"logout_confirm_title": "Log out"
},
"media_modal": {
"counter": "{current} / {total}",
@ -271,30 +271,30 @@
"previous": "Previous"
},
"moderation": {
"moderation": "Moderation",
"reports": {
"add_note": "Add note",
"close": "Close",
"delete_note": "Delete",
"delete_note_accept": "Yes, delete it",
"delete_note_cancel": "No, keep it",
"delete_note_confirm": "Are you sure you want to delete this note?",
"delete_note_title": "Confirm deletion",
"no_content": "No description given",
"no_reports": "No reports to show",
"note_placeholder": "Leave a note",
"notes": "{ count } note | { count } notes",
"reopen": "Reopen",
"report": "Report on",
"reports": "Reports",
"resolve": "Resolve",
"show_closed": "Show closed",
"statuses": "{ count } post| { count } posts",
"tag_policy_notice": "Enable the TagPolicy MRF to set post restrictions",
"tags": "Set post restrictions"
},
"statuses": "Posts",
"users": "Users"
"moderation": "Moderation",
"reports": {
"no_reports": "No reports to show",
"add_note": "Add note",
"close": "Close",
"delete_note": "Delete",
"delete_note_accept": "Yes, delete it",
"delete_note_cancel": "No, keep it",
"delete_note_confirm": "Are you sure you want to delete this note?",
"delete_note_title": "Confirm deletion",
"no_content": "No description given",
"note_placeholder": "Leave a note...",
"notes": "{ count } note | { count } notes",
"reopen": "Reopen",
"report": "Report on",
"reports": "Reports",
"resolve": "Resolve",
"show_closed": "Show closed",
"statuses": "{ count } status | { count } statuses",
"tag_policy_notice": "Enable the TagPolicy MRF to set post restrictions",
"tags": "Set post restrictions"
},
"statuses": "Statuses",
"users": "Users"
},
"nav": {
"about": "About",
@ -409,8 +409,8 @@
}
},
"registration": {
"awaiting_email_confirmation": "Your account has been registered and an email has been sent to your address. Please check the email to complete registration.",
"awaiting_email_confirmation_title": "Awaiting email confirmation",
"awaiting_email_confirmation": "Your account has been registered and an email has been sent to your address. Please check the email to complete registration.",
"bio": "Bio",
"bio_placeholder": "e.g.\nHi! Welcome to my bio.\nI love watching anime and playing games. I hope we can be friends!",
"captcha": "CAPTCHA",
@ -535,8 +535,6 @@
"enable_web_push_notifications": "Enable web push notifications",
"enter_current_password_to_confirm": "Enter your current password to confirm your identity",
"expert_mode": "Show advanced",
"expire_posts_enabled": "Delete posts after a set amount of days",
"expire_posts_input_placeholder": "Number of days",
"export_theme": "Save preset",
"file_export_import": {
"backup_restore": "Settings backup",
@ -731,18 +729,18 @@
"setting_server_side": "This setting is tied to your profile and affects all sessions and clients",
"settings": "Settings",
"settings_profile": "Settings Profiles",
"settings_profile_creation": "Create new profile",
"settings_profile_creation_new_name_label": "Name",
"settings_profile_creation_submit": "Create",
"settings_profile_currently": "Currently using {name} (version: {version})",
"settings_profiles_show": "Show all settings profiles",
"settings_profiles_unshow": "Hide all settings profiles",
"settings_profile_in_use": "In use",
"settings_profile_creation": "Create new profile",
"settings_profile_creation_submit": "Create",
"settings_profile_creation_new_name_label": "Name",
"settings_profile_use": "Use",
"settings_profile_delete": "Delete",
"settings_profile_delete_confirm": "Do you really want to delete this profile?",
"settings_profile_force_sync": "Synchronize",
"settings_profile_in_use": "In use",
"settings_profile_use": "Use",
"settings_profiles_refresh": "Reload settings profiles",
"settings_profiles_show": "Show all settings profiles",
"settings_profiles_unshow": "Hide all settings profiles",
"show_admin_badge": "Show \"Admin\" badge in my profile",
"show_moderator_badge": "Show \"Moderator\" badge in my profile",
"show_nav_shortcuts": "Show extra navigation shortcuts in top panel",
@ -939,15 +937,14 @@
"title": "Version"
},
"virtual_scrolling": "Optimize timeline rendering",
"use_blurhash": "Use blurhashes for NSFW thumbnails",
"word_filter": "Word filter",
"wordfilter": "Wordfilter"
},
"settings_profile": {
"creating": "Creating new setting profile \"{profile}\"...",
"synchronization_error": "Could not synchronize settings: {err}",
"synchronized": "Synchronized settings!",
"synchronizing": "Synchronizing setting profile \"{profile}\"..."
"synchronizing": "Synchronizing setting profile \"{profile}\"...",
"synchronized": "Synchronized settings!",
"synchronization_error": "Could not synchronize settings: {err}",
"creating": "Creating new setting profile \"{profile}\"..."
},
"status": {
"ancestor_follow": "See {numReplies} other reply under this post | See {numReplies} other replies under this post",
@ -1047,7 +1044,6 @@
"collapse": "Collapse",
"conversation": "Conversation",
"error": "Error fetching timeline: {0}",
"follow_tag": "Follow hashtag",
"load_older": "Load older posts",
"no_more_statuses": "No more posts",
"no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated or quoted",
@ -1057,8 +1053,6 @@
"show_new": "Show new",
"socket_broke": "Realtime connection lost: CloseEvent code {0}",
"socket_reconnected": "Realtime connection established",
"follow_tag": "Follow hashtag",
"unfollow_tag": "Unfollow hashtag",
"up_to_date": "Up-to-date"
},
"toast": {
@ -1123,7 +1117,6 @@
"block_confirm_title": "Block user",
"block_progress": "Blocking…",
"blocked": "Blocked!",
"blocks_you": "Blocks you!",
"bot": "Bot",
"deactivated": "Deactivated",
"deny": "Deny",
@ -1141,10 +1134,9 @@
"follow_unfollow": "Unfollow",
"followees": "Following",
"followers": "Followers",
"followed_tags": "Followed hashtags",
"followed_users": "Followed users",
"following": "Following!",
"follows_you": "Follows you!",
"requested_by": "Has requested to follow you",
"hidden": "Hidden",
"hide_repeats": "Hide repeats",
"highlight": {
@ -1171,7 +1163,6 @@
"remove_follower": "Remove follower",
"replies": "With Replies",
"report": "Report",
"requested_by": "Has requested to follow you",
"show_repeats": "Show repeats",
"statuses": "Posts",
"subscribe": "Subscribe",
@ -1181,9 +1172,6 @@
"unfollow_confirm_accept_button": "Yes, unfollow",
"unfollow_confirm_cancel_button": "No, don't unfollow",
"unfollow_confirm_title": "Unfollow user",
"not_following_any_hashtags": "You are not following any hashtags",
"follow_tag": "Follow hashtag",
"unfollow_tag": "Unfollow hashtag",
"unmute": "Unmute",
"unmute_progress": "Unmuting…",
"unsubscribe": "Unsubscribe"
@ -1191,8 +1179,7 @@
"user_profile": {
"profile_does_not_exist": "Sorry, this profile does not exist.",
"profile_loading_error": "Sorry, there was an error loading this profile.",
"timeline_title": "User timeline",
"field_validated": "Link Verified"
"timeline_title": "User timeline"
},
"user_reporting": {
"add_comment_description": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",

View file

@ -409,8 +409,6 @@
}
},
"registration": {
"awaiting_email_confirmation": "Su cuenta ha sido registrada y se ha enviado un correo electrónico a su dirección. Por favor revise el correo electrónico para completar el registro.",
"awaiting_email_confirmation_title": "En espera de confirmación por correo electrónico",
"bio": "Biografía",
"bio_placeholder": "p. ej.\nHola, soy un ejemplo.\nAquí puedes poner algo representativo tuyo... o no.",
"captcha": "CAPTCHA",
@ -424,8 +422,6 @@
"reason_placeholder": "Los registros de esta instancia son aprobados manualmente.\nComéntanos por qué quieres registrarte aquí.",
"register": "Registrarse",
"registration": "Registro",
"request_sent": "Su solicitud de registro ha sido enviada para su aprobación. Recibirá un correo electrónico cuando se apruebe su cuenta.",
"request_sent_title": "Solicitud de registro enviada",
"token": "Token de invitación",
"username_placeholder": "p. ej. akko",
"validations": {
@ -668,7 +664,6 @@
"notification_visibility_likes": "Favoritos",
"notification_visibility_mentions": "Menciones",
"notification_visibility_moves": "Usuario Migrado",
"notification_visibility_polls": "Encuestas finalizadas en las que has participado",
"notification_visibility_repeats": "Repeticiones (Repeats)",
"notifications": "Notificaciones",
"nsfw_clickthrough": "Habilitar la ocultación de la imagen de vista previa del enlace y el adjunto para los estados NSFW por defecto",
@ -677,9 +672,7 @@
"panelRadius": "Paneles",
"pause_on_unfocused": "Parar la transmisión cuando no estés en foco",
"play_videos_in_modal": "Reproducir los vídeos en un marco emergente",
"post_look_feel": "Aspecto de las publicaciones",
"post_status_content_type": "Formato predeterminado de publicación",
"posts": "Publicaciones",
"post_status_content_type": "Formato de publicación",
"preload_images": "Precargar las imágenes",
"presets": "Por defecto",
"profile_background": "Imagen de fondo del perfil",
@ -693,9 +686,6 @@
"profile_tab": "Perfil",
"radii_help": "Establezca el redondeo de las esquinas de la interfaz (en píxeles)",
"refresh_token": "Actualizar el token",
"remove_alias": "Eliminar este alias",
"remove_backup": "Eliminar",
"render_mfm": "Renderizar Markdown de Misskey",
"replies_in_timeline": "Réplicas en la línea temporal",
"reply_visibility_all": "Mostrar todas las réplicas",
"reply_visibility_following": "Solo mostrar réplicas para mí o usuarios a los que sigo",
@ -720,24 +710,10 @@
"security_tab": "Seguridad",
"sensitive_by_default": "Identificar las publicaciones como sensibles de forma predeterminada",
"set_new_avatar": "Cambiar avatar",
"set_new_mascot": "Fijar nueva mascota",
"set_new_profile_background": "Cambiar el fondo del perfil",
"set_new_profile_banner": "Cambiar la cabecera del perfil",
"setting_changed": "La configuración es diferente a la predeterminada",
"setting_server_side": "Esta configuración está vinculada a su perfil y afecta a todas las sesiones y clientes",
"settings": "Ajustes",
"settings_profile": "Ajustes de Perfiles",
"settings_profile_creation": "Crear nuevo perfil",
"settings_profile_creation_new_name_label": "Nombre",
"settings_profile_creation_submit": "Crear",
"settings_profile_currently": "Actualmente usando {nombre} (versión: {version})",
"settings_profile_delete": "Eliminar",
"settings_profile_delete_confirm": "¿Realmente quieres eliminar este perfil?",
"settings_profile_force_sync": "Sincronizar",
"settings_profile_in_use": "En uso",
"settings_profile_use": "Usar",
"settings_profiles_show": "Mostrar todos los perfiles de configuración",
"settings_profiles_unshow": "Ocultar todos los perfiles de configuración",
"show_admin_badge": "Mostrar la insignia de \"Administrador/a\" en mi perfil",
"show_moderator_badge": "Mostrar la insignia de \"Moderador/a\" en mi perfil",
"stop_gifs": "Iniciar GIFs al pasar el ratón",

View file

@ -3,7 +3,6 @@
"mrf": {
"federation": "Federasi",
"keyword": {
"ftl_removal": "Penghapusan dari Linimasa \"Jaringan Yang Dikenal\"",
"is_replaced_by": "→",
"reject": "Tolak"
},
@ -11,57 +10,28 @@
"simple": {
"accept": "Terima",
"accept_desc": "Instansi ini hanya menerima pesan dari instansi-instansi berikut:",
"ftl_removal": "Penghapusan dari Linimasa \"Jaringan Yang Dikenal\"",
"ftl_removal_desc": "Instansi ini menghapus instansi berikut dari linimasa \"Jaringan Yang Dikenal\":",
"instance": "Instansi",
"media_nsfw": "Media diatur sebagai sensitif secara paksa",
"media_nsfw_desc": "Instansi ini secara paksa menandai media pada postingan dari instansi berikut sebagai sensitif:",
"media_removal": "Penghapusan Media",
"media_removal_desc": "Instansi ini menghapus media dari postingan yang berasal dari instansi-instansi berikut:",
"not_applicable": "Tidak berlaku",
"quarantine": "Karantina",
"quarantine_desc": "Instansi ini tidak akan mengirim postingan publik ke instansi berikut:",
"reason": "Alasan",
"quarantine_desc": "Instansi ini hanya akan mengirim postingan publik ke instansi-instansi berikut:",
"reject": "Tolak",
"reject_desc": "Instansi ini tidak akan menerima pesan dari instansi-instansi berikut:",
"simple_policies": "Kebijakan khusus instansi"
"reject_desc": "Instansi ini tidak akan menerima pesan dari instansi-instansi berikut:"
}
},
"staff": "Staf"
},
"announcements": {
"all_day_prompt": "Ini adalah acara sepanjang hari",
"cancel_edit_action": "Batal",
"close_error": "Tutup",
"delete_action": "Hapus",
"edit_action": "Sunting",
"end_time_display": "Berakhir pada {time}",
"end_time_prompt": "Waktu berakhir: ",
"inactive_message": "Pengumuman ini nonaktif",
"mark_as_read_action": "Tandai sebagai dibaca",
"page_header": "Pengumuman",
"post_action": "Posting",
"post_error": "Kesalahan: {error}",
"post_form_header": "Posting pengumuman",
"post_placeholder": "Isi pengumuman",
"published_time_display": "Diterbitkan pada {time}",
"start_time_display": "Dimulai pada {time}",
"start_time_prompt": "Waktu mulai: ",
"submit_edit_action": "Kirim",
"title": "Pengumuman"
},
"chats": {
"chats": "Obrolan",
"delete": "Hapus",
"delete_confirm": "Apakah kamu benar-benar ingin menghapus pesan ini?",
"empty_chat_list_placeholder": "Kamu belum memiliki obrolan. Mulai obrolan baru!",
"delete_confirm": "Apakah Anda benar-benar ingin menghapus pesan ini?",
"empty_chat_list_placeholder": "Anda belum memiliki obrolan. Buat sbeuah obrolan baru!",
"empty_message_error": "Tidak dapat memposting pesan yang kosong",
"error_loading_chat": "Sesuatu yang salah terjadi ketika memuat obrolan.",
"error_sending_message": "Sesuatu yang salah terjadi ketika mengirim pesan.",
"message_user": "Kirim Pesan ke {nickname}",
"more": "Lebih banyak",
"new": "Obrolan Baru",
"you": "Kamu:"
"you": "Anda:"
},
"display_date": {
"today": "Hari Ini"
@ -70,7 +40,7 @@
"mute": "Bisukan",
"mute_progress": "Membisukan…",
"unmute": "Berhenti membisukan",
"unmute_progress": "Menghentikan pembisuan…"
"unmute_progress": "Memberhentikan pembisuan…"
},
"emoji": {
"add_emoji": "Sisipkan emoji",
@ -81,17 +51,16 @@
"load_all_hint": "Memuat {saneAmount} emoji pertama, memuat semua emoji dapat menyebabkan masalah performa.",
"search_emoji": "Cari emoji",
"stickers": "Stiker",
"unicode": "Emoji Unicode"
"unicode": "Emoji unicode"
},
"errors": {
"storage_unavailable": "Pleroma tidak dapat mengakses penyimpanan browser. Login kamu atau pengaturan lokal kamu tidak akan tersimpan dan masalah yang tidak terduga dapat terjadi. Coba aktifkan kuki."
"storage_unavailable": "Pleroma tidak dapat mengakses penyimpanan browser. Login Anda atau pengaturan lokal Anda tidak akan tersimpan dan masalah yang tidak terduga dapat terjadi. Coba mengaktifkan kuki."
},
"exporter": {
"export": "Ekspor",
"processing": "Memproses, kamu akan segera diminta untuk mengunduh berkas kamu"
"processing": "Memproses, Anda akan segera diminta untuk mengunduh berkas Anda"
},
"features_panel": {
"media_proxy": "Proxy media",
"text_limit": "Batas teks",
"title": "Fitur-fitur",
"upload_limit": "Batas unggahan"
@ -127,12 +96,6 @@
"admin": "Admin",
"moderator": "Moderator"
},
"scope_in_timeline": {
"direct": "Langsung",
"local": "Lokal - hanya instansi kamu yang dapat melihat postingan ini",
"private": "Hanya pengikut",
"public": "Publik"
},
"show_less": "Tampilkan lebih sedikit",
"show_more": "Tampilkan lebih banyak",
"submit": "Kirim",
@ -155,154 +118,68 @@
"load_older": "Muat interaksi yang lebih tua",
"moves": "Pengguna yang bermigrasi"
},
"languages": {
"ar": "Arab",
"az": "Azerbaijan",
"bg": "Bulgaria",
"cs": "Ceko",
"da": "Denmark",
"de": "Jerman",
"el": "Yunani",
"en": "Inggris",
"eo": "Esperanto",
"es": "Spanyol",
"fa": "Persia",
"fi": "Finlandia",
"fr": "Prancis",
"ga": "Irlandia",
"he": "Ibrani",
"hi": "Hindi",
"hu": "Hongaria",
"id": "Indonesia",
"it": "Italia",
"ja": "Jepang",
"ko": "Korea",
"lt": "Lithuania",
"lv": "Latvia",
"nl": "Belanda",
"pl": "Polandia",
"pt": "Portugal",
"ru": "Rusia",
"sk": "Slovakia",
"sv": "Swedia",
"tr": "Turki",
"translated_from": {
"ar": "Diterjemahkan dari @:languages.ar",
"bg": "Diterjemahkan dari @:languages.bg",
"en": "Diterjemahkan dari @:languages.en"
},
"uk": "Ukraina",
"zh": "Tionghoa"
},
"lists": {
"create": "Buat",
"delete": "Hapus daftar",
"lists": "Daftar",
"new": "Buat Daftar",
"save": "Simpan perubahan",
"search": "Telusuri pengguna",
"title": "Judul daftar"
},
"login": {
"authentication_code": "Kode otentikasi",
"description": "Masuk dengan OAuth",
"enter_recovery_code": "Masukkan kode pemulihan",
"enter_two_factor_code": "Masukkan kode dua-faktor",
"heading": {
"recovery": "Pemulihan dua-faktor",
"totp": "Otentikasi dua-faktor"
},
"hint": "Masuk untuk ikut berdiskusi",
"login": "Masuk",
"logout": "Keluar",
"logout_confirm": "Apa kamu yakin ingin keluar?",
"logout_confirm_accept_button": "Keluar",
"logout_confirm_cancel_button": "Batal",
"logout_confirm_title": "Keluar",
"password": "Kata sandi",
"placeholder": "namapenggunaku",
"placeholder": "contoh: lain",
"recovery_code": "Kode pemulihan",
"register": "Daftar",
"username": "Nama pengguna"
},
"media_modal": {
"counter": "{current} / {total}",
"hide": "Tutup penampil media",
"next": "Berikutnya",
"previous": "Sebelumnya"
},
"moderation": {
"moderation": "Moderasi",
"reports": {
"add_note": "Tambahkan catatan",
"close": "Tutup",
"delete_note": "Hapus",
"delete_note_accept": "Ya, hapus",
"delete_note_cancel": "Tidak, kembalikan",
"delete_note_confirm": "Apa kamu yakin ingin menghapus catatan ini?",
"delete_note_title": "Konfirmasi penghapusan",
"no_content": "Tak diberikan keterangan",
"no_reports": "Tak ada laporan",
"note_placeholder": "Tinggalkan catatan...",
"reopen": "Buka kembali",
"reports": "Laporan",
"resolve": "Selesaikan",
"show_closed": "Tampilkan yang telah ditutup"
},
"statuses": "Status",
"users": "Pengguna"
"next": "Selanjutnya",
"previous": "Sebelum"
},
"nav": {
"about": "Tentang",
"administration": "Administrasi",
"announcements": "Pengumuman",
"back": "Kembali",
"bubble_timeline_description": "Postingan dari instansi yang dekat dengan instansimu, yang direkomendasikan oleh admin kamu",
"chats": "Obrolan",
"dms": "Pesan langsung",
"friend_requests": "Ingin mengikuti",
"home_timeline": "Linimasa beranda",
"home_timeline_description": "Postingan dari orang yang kamu ikuti",
"interactions": "Interaksi",
"lists": "Daftar",
"mentions": "Sebutan",
"moderation": "Moderasi",
"preferences": "Preferensi",
"public_timeline_description": "Postingan publik dari instansi ini",
"public_tl": "Linimasa publik",
"search": "Penelusuran",
"search": "Cari",
"timeline": "Linimasa",
"timelines": "Linimasa",
"twkn": "Jaringan Yang Dikenal",
"twkn_timeline_description": "Postingan dari seluruh jaringan",
"user_search": "Penelusuran Pengguna"
"user_search": "Pencarian Pengguna"
},
"notifications": {
"broken_favorite": "Postingan tak dikenal, mencarinya…",
"broken_favorite": "Status tak diketahui, mencarinya…",
"error": "Terjadi kesalahan ketika memuat notifikasi: {0}",
"favorited_you": "memfavoritkan postinganmu",
"follow_request": "ingin mengikuti kamu",
"followed_you": "mengikuti kamu",
"favorited_you": "memfavoritkan status Anda",
"follow_request": "ingin mengikuti Anda",
"followed_you": "mengikuti Anda",
"load_older": "Muat notifikasi yang lebih lama",
"migrated_to": "bermigrasi ke",
"no_more_notifications": "Tidak ada notifikasi lagi",
"notifications": "Notifikasi",
"poll_ended": "japat telah berakhir",
"reacted_with": "bereaksi dengan {0}",
"read": "Dibaca!",
"repeated_you": "mengulangi postinganmu"
"repeated_you": "mengulangi status Anda"
},
"password_reset": {
"check_email": "Periksa surelmu untuk mendapatkan tautan yang digunakan untuk mengatur ulang kata sandimu.",
"forgot_password": "Lupa kata sandi?",
"instruction": "Masukkan surel atau nama pengguna kamu. Kami akan mengirimkan kamu tautan untuk mengatur ulang kata sandi.",
"password_reset": "Pengatur ulangan kata sandi",
"password_reset_disabled": "Pengatur-ulangan kata sandi dinonaktifkan. Silakan hubungi administrator instansi kamu.",
"password_reset_required": "Kamu harus mengatur ulang kata sandi kamu untuk masuk.",
"password_reset_required_but_mailer_is_disabled": "Kamu harus mengatur ulang kata sandi, tetapi pengatur-ulangan kata sandi dinonaktifkan. Silakan hubungi administrator instansimu.",
"placeholder": "Surel atau nama pengguna kamu",
"instruction": "Masukkan surel atau nama pengguna Anda. Kami akan mengirimkan Anda tautan untuk mengatur ulang kata sandi.",
"password_reset": "Pengatur-ulangan kata sandi",
"password_reset_disabled": "Pengatur-ulangan kata sandi dinonaktifkan. Hubungi administrator instansi Anda.",
"password_reset_required": "Anda harus mengatur ulang kata sandi Anda untuk masuk.",
"password_reset_required_but_mailer_is_disabled": "Anda harus mengatur ulang kata sandi, tetapi pengatur-ulangan kata sandi dinonaktifkan. Silakan hubungi administrator instansi Anda.",
"placeholder": "Surel atau nama pengguna Anda",
"return_home": "Kembali ke halaman beranda",
"too_many_requests": "Kamu telah mencapai batas percobaan, coba lagi nanti."
"too_many_requests": "Anda telah mencapai batas percobaan, coba lagi nanti."
},
"polls": {
"add_option": "Tambahkan opsi",
@ -313,83 +190,67 @@
"not_enough_options": "Terlalu sedikit opsi yang unik pada japat",
"option": "Opsi",
"people_voted_count": "{count} orang memilih | {count} orang memilih",
"single_choice": "",
"type": "Jenis japat",
"vote": "Pilih",
"votes": "suara",
"votes_count": "{count} suara | {count} suara"
},
"post_status": {
"account_not_locked_warning": "Akun kamu tidak {0}. Siapapun dapat mengikuti kamu untuk melihat postingan hanya-pengikut kamu.",
"account_not_locked_warning": "Akun Anda tidak {0}. Siapapun dapat mengikuti Anda untuk melihat postingan hanya-pengikut Anda.",
"account_not_locked_warning_link": "terkunci",
"attachments_sensitive": "Tandai lampiran sebagai sensitif",
"content_type": {
"text/bbcode": "BBCode",
"text/html": "HTML",
"text/markdown": "Markdown",
"text/plain": "Teks biasa",
"text/x.misskeymarkdown": "MFM"
"text/plain": "Teks biasa"
},
"content_warning": "Peringatan Konten (opsional)",
"default": "Baru saja tiba di Luna Nova Academy",
"content_warning": "Subyek (opsional)",
"default": "Baru saja mendarat di L.A.",
"direct_warning_to_all": "Postingan ini akan terlihat oleh pengguna yang disebutkan.",
"direct_warning_to_first_only": "Postingan ini akan terlihat oleh pengguna yang disebutkan di awal pesan.",
"edit_remote_warning": "Perubahan yang dibuat pada postingan ini mungkin tidak terlihat pada beberapa instansi!",
"edit_status": "Sunting Status",
"edit_unsupported_warning": "Japat dan sebutan tidak bisa diubah dengan menyunting.",
"empty_status_error": "Tidak dapat memposting tanpa isi atau berkas",
"empty_status_error": "Tidak dapat memposting status kosong tanpa berkas",
"media_description": "Keterangan media",
"media_description_error": "Gagal memperbarui media, coba lagi",
"media_not_sensitive_warning": "Kamu memasang Peringatan Konten, namun lampirannya tidak ditandai sebagai sensitif!",
"new_status": "Posting",
"new_status": "Posting status baru",
"post": "Posting",
"posting": "Memposting",
"preview": "Pratinjau",
"preview_empty": "Kosong",
"scope": {
"direct": "Langsung - posting hanya kepada pengguna yang disebut",
"local": "Lokal - postingan tidak akan difederasi",
"private": "Hanya-pengikut - posting hanya kepada pengikut",
"public": "Publik - posting ke linimasa publik"
},
"scope_notice": {
"local": "Postingan ini tidak akan terlihat di instansi lain",
"private": "Postingan ini akan terlihat hanya oleh pengikut kamu",
"public": "Postingan ini akan terlihat oleh siapa saja",
"unlisted": "Postingan ini tidak akan terlihat di Linimasa Publik dan Jaringan Yang Dikenal"
"private": "Postingan ini akan terlihat hanya oleh pengikut Anda",
"public": "Postingan ini akan terlihat oleh siapa saja"
}
},
"registration": {
"awaiting_email_confirmation": "Akunmu telah terdaftar dan sebuah surel telah dikirimkan ke alamat kamu. Harap periksa surel untuk menyelesaikan pendaftaran.",
"awaiting_email_confirmation_title": "Menunggu konfirmasi surel",
"bio": "Bio",
"bio_placeholder": "cth.\nHai! Selamat datang di bioku.\nAku suka menonton anime dan bermain game. Semoga kita bisa berteman!",
"bio_placeholder": "contoh.\nHai, aku Lain.\nAku seorang putri anime yang tinggal di pinggiran kota Jepang. Kamu mungkin mengenal aku dari Wired.",
"captcha": "CAPTCHA",
"email": "Surel",
"email_language": "Dalam bahasa apa kamu ingin menerima surel dari server ini?",
"fullname_placeholder": "cth. Atsuko Kagari",
"fullname_placeholder": "contoh. Lain Iwakura",
"new_captcha": "Klik gambarnya untuk mendapatkan captcha baru",
"password_confirm": "Konfirmasi kata sandi",
"reason": "Alasan mendaftar",
"reason_placeholder": "Instansi ini menerima pendaftaran secara manual.\nBeritahu administrasinya mengapa kamu ingin mendaftar.",
"reason_placeholder": "Instansi ini menerima pendaftaran secara manual.\nBeritahu administrasinya mengapa Anda ingin mendaftar.",
"register": "Daftar",
"registration": "Pendaftaran",
"request_sent": "Permintaan pendaftaran kamu telah dikirim untuk diperiksa. Kamu akan menerima surel saat akunmu diterima.",
"request_sent_title": "Permintaan pendaftaran dikirim",
"token": "Token undangan",
"username_placeholder": "cth. akko",
"username_placeholder": "contoh. lain",
"validations": {
"email_required": "tidak boleh kosong",
"fullname_required": "tidak boleh kosong",
"password_confirmation_match": "harus sama dengan kata sandi",
"password_confirmation_required": "tidak boleh kosong",
"password_required": "tidak boleh kosong",
"username_required": "tidak boleh kosong"
}
},
"remote_user_resolver": {
"error": "Tidak ditemukan.",
"searching_for": "Mencari"
"error": "Tidak ditemukan."
},
"search": {
"hashtags": "Tagar",
@ -402,12 +263,6 @@
"select_all": "Pilih semua"
},
"settings": {
"account_backup": "Pencadangan akun",
"account_backup_description": "Ini memungkinkan kamu untuk mengunduh arsip yang berisi informasi tentang akun dan postingan kamu, namun belum bisa diimpor ke akun Pleroma.",
"account_privacy": "Privasi",
"add_backup": "Buat cadangan baru",
"added_backup": "Cadangan baru ditambahkan.",
"allow_following_move": "Ikuti otomatis apabila akun yang diikuti pindah",
"app_name": "Nama aplikasi",
"attachmentRadius": "Lampiran",
"attachments": "Lampiran",
@ -416,10 +271,9 @@
"avatarRadius": "Avatar",
"avatar_size_instruction": "Ukuran minimum gambar avatar yang disarankan adalah 150x150 piksel.",
"background": "Latar belakang",
"backup_not_ready": "Cadangan ini belum siap.",
"bio": "Bio",
"block_export": "Ekspor blokiran",
"block_export_button": "Ekspor blokiranmu menjadi berkas csv",
"block_export_button": "Ekspor blokiran Anda menjadi berkas csv",
"block_import": "Impor blokiran",
"block_import_error": "Terjadi kesalahan ketika mengimpor blokiran",
"blocks_imported": "Blokiran diimpor! Pemrosesannya mungkin memakan sedikit waktu.",
@ -431,47 +285,24 @@
"cOrange": "Jingga (Favorit)",
"cRed": "Merah (Batal)",
"change_email": "Ubah surel",
"change_email_error": "Ada masalah ketika mengubah surel kamu.",
"change_email_error": "Ada masalah ketika mengubah surel Anda.",
"change_password": "Ubah kata sandi",
"change_password_error": "Ada masalah ketika mengubah kata sandi kamu.",
"change_password_error": "Ada masalah ketika mengubah kata sandi Anda.",
"changed_email": "Surel berhasil diubah!",
"changed_password": "Kata sandi berhasil diubah!",
"chatMessageRadius": "Pesan obrolan",
"checkboxRadius": "Kotak centang",
"composing": "Menulis",
"confirm_dialogs": "Perlukan konfirmasi sebelum:",
"confirm_dialogs_approve_follow": "Menerima permintaan mengikuti",
"confirm_dialogs_block": "Memblokir seseorang",
"confirm_dialogs_delete": "Menghapus postingan",
"confirm_dialogs_deny_follow": "Menolak permintaan mengikuti",
"confirm_dialogs_mute": "Membisukan seseorang",
"confirm_dialogs_repeat": "Mengulangi postingan",
"confirm_dialogs_unfollow": "Berhenti mengikuti seseorang",
"confirm_new_password": "Konfirmasi kata sandi baru",
"conversation_display": "Gaya tampilan obrolan",
"current_password": "Kata sandi saat ini",
"data_import_export_tab": "Impor / ekspor data",
"delete_account": "Hapus akun",
"delete_account_description": "Hapus data kamu secara permanen dan nonaktifkan akunmu.",
"delete_account_error": "Ada masalah ketika menghapus akun kamu. Jika ini terus terjadi harap hubungi adminstrator instansi kamu.",
"delete_account_instructions": "Ketik kata sandi kamu pada input di bawah untuk mengonfirmasi penghapusan akun.",
"discoverable": "Izinkan penelusuran akun ini pada hasil pencarian dan layanan lainnya",
"delete_account_description": "Hapus data Anda secara permanen dan menonaktifkan akun Anda.",
"delete_account_error": "Ada masalah ketika menghapus akun Anda. Jika ini terus terjadi harap hubungi adminstrator instansi Anda.",
"delete_account_instructions": "Ketik kata sandi Anda pada input di bawah untuk mengkonfirmasi penghapusan akun.",
"domain_mutes": "Domain",
"download_backup": "Unduh",
"email_language": "Bahasa yang digunakan untuk menerima surel dari server ini",
"emoji_reactions_on_timeline": "Tampilkan reaksi emoji pada linimasa",
"enable_web_push_notifications": "Aktifkan notifikasi push web",
"enter_current_password_to_confirm": "Masukkan kata sandi kamu saat ini untuk mengonfirmasi identitas kamu",
"expire_posts_enabled": "Hapus postingan setelah jumlah hari yang ditentukan",
"expire_posts_input_placeholder": "Jumlah hari",
"file_export_import": {
"backup_restore": "Pencadangan pengaturan",
"backup_settings": "Cadangkan pengaturan ke berkas",
"backup_settings_theme": "Cadangkan pengaturan dan tema ke berkas",
"errors": {
"file_slightly_new": "Versi minor berkas berbeda, beberapa pengaturan mungkin tidak termuat"
}
},
"enter_current_password_to_confirm": "Masukkan kata sandi Anda saat ini untuk mengkonfirmasi identitas Anda",
"filtering": "Penyaringan",
"follow_import_error": "Terjadi kesalahan ketika mengimpor pengikut",
"fun": "Seru",
@ -481,17 +312,17 @@
"hide_follows_count_description": "Jangan tampilkan jumlah mengikuti",
"hide_follows_description": "Jangan tampilkan siapa yang saya ikuti",
"hide_muted_posts": "Sembunyikan postingan-postingan dari pengguna yang dibisukan",
"hide_post_stats": "Sembunyikan statistik postingan (seperti jumlah favorit)",
"hide_post_stats": "Sembunyikan statistik postingan (contoh. jumlah favorit)",
"hide_shoutbox": "Sembunyikan kotak suara instansi",
"hide_user_stats": "Sembunyikan statistik pengguna (seperti jumlah pengikut)",
"hide_user_stats": "Sembunyikan statistik pengguna (contoh. jumlah pengikut)",
"hide_wallpaper": "Sembunyikan latar belakang instansi",
"import_blocks_from_a_csv_file": "Impor blokiran dari berkas csv",
"instance_default": "(bawaan: {value})",
"instance_default_simple": "(bawaan)",
"interface": "Antarmuka",
"interfaceLanguage": "Bahasa antarmuka",
"invalid_theme_imported": "Berkas yang dipilih bukan sebuah tema yang didukung Pleroma. Tidak ada perubahan yang dibuat pada tema kamu.",
"limited_availability": "Tidak tersedia di browser kamu",
"invalid_theme_imported": "Berkas yang dipilih bukan sebuah tema yang didukung Pleroma. Tidak ada perbuahan yang dibuat pada tema Anda.",
"limited_availability": "Tidak tersedia di browser Anda",
"links": "Tautan",
"loop_video": "Ulang-ulang video",
"loop_video_silent_only": "Ulang-ulang video tanpa suara (seperti \"gif\" Mastodon)",
@ -502,17 +333,17 @@
"generate_new_recovery_codes": "Hasilkan kode pemulihan baru",
"otp": "OTP",
"recovery_codes": "Kode pemulihan.",
"recovery_codes_warning": "Tulis kodenya atau simpan mereka di tempat yang aman - jika tidak kamu tidak akan melihat mereka lagi. Jika kamu tidak dapat mengakses aplikasi 2FA kamu dan kode pemulihanmu hilang, kamu tidak akan bisa mengakses akun kamu.",
"recovery_codes_warning": "Tulis kode-kode nya atau simpan mereka di tempat yang aman - jika tidak Anda tidak akan melihat mereka lagi. Jika Anda tidak dapat mengakses aplikasi 2FA Anda dan kode pemulihan Anda hilang Anda tidak akan bisa mengakses akun Anda.",
"scan": {
"title": "Pindai"
},
"setup_otp": "Siapkan OTP",
"title": "Otentikasi Dua-faktor",
"verify": {
"desc": "Untuk mengaktifkan otentikasi dua-faktor, masukkan kode dari aplikasi dua-faktor kamu:"
"desc": "Untuk mengaktifkan otentikasi dua-faktor, masukkan kode dari aplikasi dua-faktor Anda:"
},
"waiting_a_recovery_codes": "Menerima kode cadangan…",
"warning_of_generate_new_codes": "Ketika kamu menghasilkan kode pemulihan baru, kode lama kamu berhenti bekerja."
"warning_of_generate_new_codes": "Ketika Anda menghasilkan kode pemulihan baru, kode lama Anda berhenti bekerja."
},
"more_settings": "Lebih banyak pengaturan",
"mutes_and_blocks": "Bisuan dan Blokiran",
@ -547,8 +378,8 @@
"save": "Simpan perubahan",
"saving_err": "Terjadi kesalahan ketika menyimpan pengaturan",
"saving_ok": "Pengaturan disimpan",
"search_user_to_block": "Cari siapa yang ingin kamu blokir",
"search_user_to_mute": "Cari siapa yang ingin kamu bisukan",
"search_user_to_block": "Cari siapa yang Anda ingin blokir",
"search_user_to_mute": "Cari siapa yang ingin Anda bisukan",
"security": "Keamanan",
"security_tab": "Keamanan",
"set_new_avatar": "Tetapkan avatar baru",
@ -616,9 +447,9 @@
"switcher": {
"help": {
"fe_upgraded": "Mesin tema PleromaFE diperbarui setelah pembaruan versi.",
"future_version_imported": "Berkas yang kamu impor dibuat pada versi FE yang lebih baru.",
"older_version_imported": "Berkas yang kamu impor dibuat pada versi FE yang lebih lama.",
"upgraded_from_v2": "PleromaFE telah diperbarui, tema dapat terlihat sedikit berbeda dari apa yang kamu ingat."
"future_version_imported": "Berkas yang Anda impor dibuat pada versi FE yang lebih baru.",
"older_version_imported": "Berkas yang Anda impor dibuat pada versi FE yang lebih lama.",
"upgraded_from_v2": "PleromaFE telah diperbarui, tema dapat terlihat sedikit berbeda dari apa yang Anda ingat."
},
"load_theme": "Muat tema",
"use_snapshot": "Versi lama",
@ -650,7 +481,7 @@
},
"status": {
"delete": "Hapus status",
"delete_confirm": "Apakah kamu benar-benar ingin menghapus postingan ini?",
"delete_confirm": "Apakah Anda benar-benar ingin menghapus status ini?",
"favorites": "Favorit",
"hide_content": "",
"mute_conversation": "Bisukan percakapan",
@ -693,7 +524,7 @@
"conversation": "Percakapan",
"error": "Terjadi kesalahan memuat linimasa: {0}",
"no_more_statuses": "Tidak ada status lagi",
"no_retweet_hint": "Postingan ditandai sebagai hanya-pengikut atau langsung dan tidak dapat diulang atau dikutip",
"no_retweet_hint": "Postingan ditandai sebagai hanya-pengikut atau langsung dan tidak dapat diulang",
"no_statuses": "Tidak ada status",
"reload": "Muat ulang",
"repeated": "diulangi"
@ -745,10 +576,10 @@
"followees": "Mengikuti",
"followers": "Pengikut",
"following": "Diikuti!",
"follows_you": "Mengikuti kamu!",
"follows_you": "Mengikuti Anda!",
"hidden": "Disembunyikan",
"hide_repeats": "Sembunyikan ulangan",
"its_you": "Ini kamu!",
"its_you": "Ini Anda!",
"media": "Media",
"mention": "Sebut",
"message": "Kirimkan pesan",
@ -768,14 +599,14 @@
"timeline_title": "Linimasa pengguna"
},
"user_reporting": {
"add_comment_description": "Laporan ini akan dikirim ke moderator instansi kamu. Kamu dapat menyediakan penjelasan mengapa kamu melaporkan akun ini di bawah:",
"add_comment_description": "Laporan ini akan dikirim ke moderator instansi Anda. Anda dapat menyediakan penjelasan mengapa Anda melaporkan akun ini di bawah:",
"additional_comments": "Komentar tambahan",
"forward_description": "Akun ini berada di server lain. Kirim salinan dari laporannya juga?",
"generic_error": "Sebuah kesalahan terjadi ketika memproses permintaan kamu.",
"generic_error": "Sebuah kesalahan terjadi ketika memproses permintaan Anda.",
"submit": "Kirim",
"title": "Melaporkan {0}"
},
"who_to_follow": {
"more": "Lebih banyak"
}
}
}

View file

@ -283,18 +283,13 @@
"no_content": "説明なし",
"no_reports": "通報なし",
"note_placeholder": "メモ",
"notes": "{count}件",
"notes": "メモ",
"reopen": "再開",
"report": "通報:",
"reports": "通報",
"resolve": "完了",
"show_closed": "完了した通報を表示",
"statuses": "{count}件",
"tag_policy_notice": "TagPolicyのMRFをONにしてください",
"tags": "ポスト制限を付ける"
},
"statuses": "ポスト",
"users": "ユーザー"
"show_closed": "完了した通報を表示"
}
},
"nav": {
"about": "このインスタンスについて",
@ -409,8 +404,6 @@
}
},
"registration": {
"awaiting_email_confirmation": "あなたにメールが送られました。メールをご覧くださって、リンクをクリックしてください",
"awaiting_email_confirmation_title": "メール確認中",
"bio": "プロフィール",
"bio_placeholder": "例:\nこんにちは。私は玲音。\n私はアニメのキャラクターで、日本の郊外に住んでいます。私をWiredで見たことがあるかもしれません。",
"captcha": "CAPTCHA",
@ -424,8 +417,6 @@
"reason_placeholder": "このインスタンスは、新規登録を手動で受け付けています。\n登録したい理由を、インスタンスの管理者に教えてください。",
"register": "登録",
"registration": "登録",
"request_sent": "登録リクエストを送りました。登録受け入れたらメールが届きます。",
"request_sent_title": "登録リクエストを送りました",
"token": "招待トークン",
"username_placeholder": "例: lain",
"validations": {
@ -535,8 +526,6 @@
"enable_web_push_notifications": "ウェブプッシュ通知を許可する",
"enter_current_password_to_confirm": "あなたのアイデンティティを証明するため、現在のパスワードを入力してください",
"expert_mode": "詳細設定を表示",
"expire_posts_enabled": "自動削除",
"expire_posts_input_placeholder": "日数",
"export_theme": "保存",
"file_export_import": {
"backup_restore": "設定をバックアップ",
@ -1046,7 +1035,6 @@
"collapse": "たたむ",
"conversation": "スレッド",
"error": "タイムラインの読み込みに失敗しました: {0}",
"follow_tag": "タグをフォロー",
"load_older": "古い投稿",
"no_more_statuses": "これで終わりです",
"no_retweet_hint": "投稿を「フォロワーのみ」または「ダイレクト」にすると、リピートできなくなります",
@ -1056,7 +1044,6 @@
"show_new": "読み込み",
"socket_broke": "コード{0}によりリアルタイム接続が切断されました",
"socket_reconnected": "リアルタイム接続が確立されました",
"unfollow_tag": "タグのフォローを解除",
"up_to_date": "最新"
},
"toast": {

View file

@ -1,113 +1,12 @@
import { createStore } from 'vuex'
import getStore from './store';
import i18n from './i18n';
import 'custom-event-polyfill'
import './lib/event_target_polyfill.js'
import interfaceModule from './modules/interface.js'
import instanceModule from './modules/instance.js'
import statusesModule from './modules/statuses.js'
import listsModule from './modules/lists.js'
import usersModule from './modules/users.js'
import apiModule from './modules/api.js'
import configModule from './modules/config.js'
import serverSideConfigModule from './modules/serverSideConfig.js'
import oauthModule from './modules/oauth.js'
import authFlowModule from './modules/auth_flow.js'
import mediaViewerModule from './modules/media_viewer.js'
import oauthTokensModule from './modules/oauth_tokens.js'
import reportsModule from './modules/reports.js'
import pollsModule from './modules/polls.js'
import postStatusModule from './modules/postStatus.js'
import announcementsModule from './modules/announcements.js'
import editStatusModule from './modules/editStatus.js'
import statusHistoryModule from './modules/statusHistory.js'
import tagModule from './modules/tags.js'
import { createI18n } from 'vue-i18n'
import createPersistedState from './lib/persisted_state.js'
import pushNotifications from './lib/push_notifications_plugin.js'
import messages from './i18n/messages.js'
import afterStoreSetup from './boot/after_store.js'
const currentLocale = (window.navigator.language || 'en').split('-')[0]
const i18n = createI18n({
// By default, use the browser locale, we will update it if neccessary
locale: 'en',
fallbackLocale: 'en',
messages: messages.default
})
messages.setLanguage(i18n, currentLocale)
const persistedStateOptions = {
paths: [
'config',
'users.lastLoginName',
'oauth'
]
};
(async () => {
if ('serviceWorker' in navigator) {
// declaring scope manually
navigator.serviceWorker.register('/sw-pleroma.js', {scope: '/'}).then((registration) => {
console.log('Service worker registration succeeded:', registration);
}, /*catch*/ (error) => {
console.error(`Service worker registration failed: ${error}`);
});
} else {
console.error('Service workers are not supported.');
}
let storageError = false
const plugins = [pushNotifications]
try {
const persistedState = await createPersistedState(persistedStateOptions)
plugins.push(persistedState)
} catch (e) {
console.error(e)
storageError = true
}
const store = createStore({
modules: {
i18n: {
getters: {
i18n: () => i18n.global
}
},
interface: interfaceModule,
instance: instanceModule,
// TODO refactor users/statuses modules, they depend on each other
users: usersModule,
statuses: statusesModule,
lists: listsModule,
api: apiModule,
config: configModule,
serverSideConfig: serverSideConfigModule,
oauth: oauthModule,
authFlow: authFlowModule,
mediaViewer: mediaViewerModule,
oauthTokens: oauthTokensModule,
reports: reportsModule,
polls: pollsModule,
postStatus: postStatusModule,
announcements: announcementsModule,
editStatus: editStatusModule,
statusHistory: statusHistoryModule,
tags: tagModule
},
plugins,
strict: false // Socket modifies itself, let's ignore this for now.
// strict: process.env.NODE_ENV !== 'production'
})
if (storageError) {
store.dispatch('pushGlobalNotice', { messageKey: 'errors.storage_unavailable', level: 'error' })
}
afterStoreSetup({ store, i18n })
const store = await getStore();
return afterStoreSetup({ store, i18n })
})()
// These are inlined by webpack's DefinePlugin

View file

@ -1,6 +1,5 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { WSConnectionStatus } from '../services/api/api.service.js'
import { map } from 'lodash'
const retryTimeout = (multiplier) => 1000 * multiplier
@ -41,6 +40,9 @@ const api = {
setSocket (state, socket) {
state.socket = socket
},
setFollowRequests (state, value) {
state.followRequests = value
},
setMastoUserSocketStatus (state, value) {
state.mastoUserSocketStatus = value
},
@ -49,15 +51,6 @@ const api = {
},
resetRetryMultiplier (state) {
state.retryMultiplier = 1
},
setFollowRequests (state, value) {
state.followRequests = [...value]
},
saveFollowRequests (state, requests) {
state.followRequests = [...state.followRequests, ...requests]
},
saveFollowRequestPagination (state, pagination) {
state.followRequestsPagination = pagination
}
},
actions: {
@ -247,22 +240,24 @@ const api = {
...rest
})
},
// Follow requests
startFetchingFollowRequests (store) {
if (store.state.fetchers['followRequests']) return
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
},
stopFetchingFollowRequests (store) {
const fetcher = store.state.fetchers.followRequests
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'followRequests', fetcher })
},
removeFollowRequest (store, request) {
let requests = [...store.state.followRequests].filter((it) => it.id !== request.id)
let requests = store.state.followRequests.filter((it) => it !== request)
store.commit('setFollowRequests', requests)
},
fetchFollowRequests ({ rootState, commit }) {
const pagination = rootState.api.followRequestsPagination
return rootState.api.backendInteractor.getFollowRequests({ pagination })
.then((requests) => {
if (requests.data.length > 0) {
commit('addNewUsers', requests.data)
commit('saveFollowRequests', requests.data)
commit('saveFollowRequestPagination', requests.pagination)
}
return requests
})
},
// Lists
startFetchingLists (store) {
if (store.state.fetchers['lists']) return

View file

@ -117,8 +117,7 @@ export const defaultState = {
maxDepthInThread: undefined, // instance default
translationLanguage: undefined, // instance default,
supportedTranslationLanguages: {}, // instance default
userProfileDefaultTab: 'statuses',
useBlurhash: true,
userProfileDefaultTab: 'statuses'
}
// caching the instance default properties

View file

@ -178,7 +178,7 @@ const instance = {
async getCustomEmoji ({ commit, state }) {
try {
const res = await window.fetch('/api/v1/pleroma/emoji')
const res = await window.fetch('/api/pleroma/emoji.json')
if (res.ok) {
const result = await res.json()
const values = Array.isArray(result) ? Object.assign({}, ...result) : result

View file

@ -186,7 +186,7 @@ const interfaceMod = {
if (thirdColumnMode === 'none' || !rootState.users.currentUser) {
commit('setLayoutType', normalOrMobile)
} else {
const wideLayout = width >= 1280
const wideLayout = width >= 1300
commit('setLayoutType', wideLayout ? 'wide' : normalOrMobile)
}
},

View file

@ -1,43 +0,0 @@
import { merge } from 'lodash'
const tags = {
state: {
// Contains key = name, value = tag json
tags: {}
},
getters: {
findTag: state => query => {
const result = state.tags[query]
return result
},
},
mutations: {
setTag (state, { name, data }) {
state.tags[name] = data
}
},
actions: {
getTag ({ rootState, commit }, tagName) {
return rootState.api.backendInteractor.getHashtag({ tag: tagName }).then(tag => {
commit('setTag', { name: tagName, data: tag })
return tag
})
},
followTag ({ rootState, commit }, tagName) {
return rootState.api.backendInteractor.followHashtag({ tag: tagName })
.then((resp) => {
commit('setTag', { name: tagName, data: resp })
return resp
})
},
unfollowTag ({ rootState, commit }, tagName) {
return rootState.api.backendInteractor.unfollowHashtag({ tag: tagName })
.then((resp) => {
commit('setTag', { name: tagName, data: resp })
return resp
})
}
}
}
export default tags

View file

@ -5,9 +5,9 @@ import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'loda
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
// TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item, key = 'id') => {
export const mergeOrAdd = (arr, obj, item) => {
if (!item) { return false }
const oldItem = obj[item[key]]
const oldItem = obj[item.id]
if (oldItem) {
// We already have this, so only merge the new info.
mergeWith(oldItem, item, mergeArrayLength)
@ -15,7 +15,7 @@ export const mergeOrAdd = (arr, obj, item, key = 'id') => {
} else {
// This is a new item, prepare it
arr.push(item)
obj[item[key]] = item
obj[item.id] = item
if (item.screen_name && !item.screen_name.includes('@')) {
obj[item.screen_name.toLowerCase()] = item
}
@ -157,14 +157,6 @@ export const mutations = {
const user = state.usersObject[id]
user.followerIds = uniq(concat(user.followerIds || [], followerIds))
},
saveFollowedTagIds (state, { id, followedTagIds }) {
const user = state.usersObject[id]
user.followedTagIds = uniq(concat(user.followedTagIds || [], followedTagIds))
},
saveFollowedTagPagination (state, { id, pagination }) {
const user = state.usersObject[id]
user.followedTagPagination = pagination
},
// Because frontend doesn't have a reason to keep these stuff in memory
// outside of viewing someones user profile.
clearFriends (state, userId) {
@ -179,12 +171,6 @@ export const mutations = {
user['followerIds'] = []
}
},
clearFollowedTags (state, userId) {
const user = state.usersObject[userId]
if (user) {
user['followedTagIds'] = []
}
},
addNewUsers (state, users) {
each(users, (user) => {
if (user.relationship) {
@ -265,12 +251,6 @@ export const mutations = {
signUpFailure (state, errors) {
state.signUpPending = false
state.signUpErrors = errors
},
decrementFollowRequestsCount (store) {
store.currentUser.follow_requests_count--
},
incrementFollowRequestsCount (store) {
store.currentUser.follow_requests_count++
}
}
@ -291,7 +271,7 @@ export const getters = {
relationship: state => id => {
const rel = id && state.relationships[id]
return rel || { id, loading: true }
},
}
}
export const defaultState = {
@ -302,9 +282,7 @@ export const defaultState = {
usersObject: {},
signUpPending: false,
signUpErrors: [],
relationships: {},
tags: [],
tagsObject: {}
relationships: {}
}
const users = {
@ -424,27 +402,12 @@ const users = {
return followers
})
},
fetchFollowedTags ({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const pagination = user.followedTagPagination
return rootState.api.backendInteractor.getFollowedHashtags({ pagination })
.then(({ data: tags, pagination }) => {
each(tags, tag => commit('setTag', { name: tag.name, data: tag }))
commit('saveFollowedTagIds', { id, followedTagIds: tags.map(tag => tag.name) })
commit('saveFollowedTagPagination', { id, pagination })
return tags
})
},
clearFriends ({ commit }, userId) {
commit('clearFriends', userId)
},
clearFollowers ({ commit }, userId) {
commit('clearFollowers', userId)
},
clearFollowedTags ({ commit }, userId) {
commit('clearFollowedTags', userId)
},
subscribeUser ({ rootState, commit }, id) {
return rootState.api.backendInteractor.subscribeUser({ id })
.then((relationship) => commit('updateUserRelationship', [relationship]))
@ -510,12 +473,6 @@ const users = {
store.commit('setUserForNotification', notification)
})
},
decrementFollowRequestsCount (store) {
store.commit('decrementFollowRequestsCount')
},
incrementFollowRequestsCount (store) {
store.commit('incrementFollowRequestsCount')
},
searchUsers ({ rootState, commit }, { query }) {
return rootState.api.backendInteractor.searchUsers({ query })
.then((users) => {
@ -579,6 +536,7 @@ const users = {
store.dispatch('stopFetchingTimeline', 'friends')
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
store.dispatch('stopFetchingNotifications')
store.dispatch('stopFetchingFollowRequests')
store.dispatch('stopFetchingConfig')
store.commit('clearNotifications')
store.commit('resetStatuses')
@ -604,6 +562,9 @@ const users = {
commit('addNewUsers', [user])
store.dispatch('fetchEmoji')
store.dispatch('fetchMutes')
store.dispatch('startFetchingAnnouncements')
store.dispatch('startFetchingReports')
getNotificationPermission()
.then(permission => commit('setNotificationPermission', permission))

View file

@ -1,7 +1,6 @@
import { each, map, concat, last, get } from 'lodash'
import { parseStatus, parseSource, parseUser, parseNotification, parseReport, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
import { RegistrationError, StatusCodeError } from '../errors/errors'
import { Url } from 'url'
/* eslint-env browser */
const MUTES_IMPORT_URL = '/api/pleroma/mutes_import'
@ -12,11 +11,11 @@ const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
const ALIASES_URL = '/api/pleroma/aliases'
const TAG_USER_URL = '/api/v1/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/v1/pleroma/admin/users/${screenName}/permission_group/${right}`
const ACTIVATE_USER_URL = '/api/v1/pleroma/admin/users/activate'
const DEACTIVATE_USER_URL = '/api/v1/pleroma/admin/users/deactivate'
const ADMIN_USERS_URL = '/api/v1/pleroma/admin/users'
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
const ADMIN_USERS_URL = '/api/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
@ -109,10 +108,6 @@ const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements
const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
const AKKOMA_SETTING_PROFILE_URL = (name) => `/api/v1/akkoma/frontend_settings/pleroma-fe/${name}`
const AKKOMA_SETTING_PROFILE_LIST = `/api/v1/akkoma/frontend_settings/pleroma-fe`
const MASTODON_TAG_URL = (name) => `/api/v1/tags/${name}`
const MASTODON_FOLLOW_TAG_URL = (name) => `/api/v1/tags/${name}/follow`
const MASTODON_UNFOLLOW_TAG_URL = (name) => `/api/v1/tags/${name}/unfollow`
const MASTODON_FOLLOWED_TAGS_URL = '/api/v1/followed_tags'
const oldfetch = window.fetch
@ -248,7 +243,7 @@ const register = ({ params, credentials }) => {
})
}
const getCaptcha = () => fetch('/api/v1/pleroma/captcha').then(resp => resp.json())
const getCaptcha = () => fetch('/api/pleroma/captcha').then(resp => resp.json())
const authHeaders = (accessToken) => {
if (accessToken) {
@ -406,6 +401,14 @@ const fetchFollowers = ({ id, maxId, sinceId, limit = 20, credentials }) => {
.then((data) => data.json())
.then((data) => data.map(parseUser))
}
const fetchFollowRequests = ({ credentials }) => {
const url = MASTODON_FOLLOW_REQUESTS_URL
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
.then((data) => data.map(parseUser))
}
const fetchLists = ({ credentials }) => {
const url = MASTODON_LISTS_URL
return fetch(url, { headers: authHeaders(credentials) })
@ -872,8 +875,7 @@ const postStatus = ({
quoteId,
contentType,
preview,
idempotencyKey,
language
idempotencyKey
}) => {
const form = new FormData()
const pollOptions = poll.options || []
@ -884,7 +886,6 @@ const postStatus = ({
if (visibility) form.append('visibility', visibility)
if (sensitive) form.append('sensitive', sensitive)
if (contentType) form.append('content_type', contentType)
if (language) form.append('language', language)
mediaIds.forEach(val => {
form.append('media_ids[]', val)
})
@ -1331,7 +1332,7 @@ const fetchEmojiReactions = ({ id, credentials }) => {
const reactWithEmoji = ({ id, emoji, credentials }) => {
return promisedRequest({
url: PLEROMA_EMOJI_REACT_URL(id, encodeURIComponent(emoji)),
url: PLEROMA_EMOJI_REACT_URL(id, emoji),
method: 'PUT',
credentials
}).then(parseStatus)
@ -1339,7 +1340,7 @@ const reactWithEmoji = ({ id, emoji, credentials }) => {
const unreactWithEmoji = ({ id, emoji, credentials }) => {
return promisedRequest({
url: PLEROMA_EMOJI_UNREACT_URL(id, encodeURIComponent(emoji)),
url: PLEROMA_EMOJI_UNREACT_URL(id, emoji),
method: 'DELETE',
credentials
}).then(parseStatus)
@ -1548,71 +1549,6 @@ const listSettingsProfiles = ({ credentials }) => {
})
}
const getHashtag = ({ tag, credentials }) => {
return promisedRequest({
url: MASTODON_TAG_URL(tag),
credentials
})
}
const followHashtag = ({ tag, credentials }) => {
return promisedRequest({
url: MASTODON_FOLLOW_TAG_URL(tag),
method: 'POST',
credentials
})
}
const unfollowHashtag = ({ tag, credentials }) => {
return promisedRequest({
url: MASTODON_UNFOLLOW_TAG_URL(tag),
method: 'POST',
credentials
})
}
const getFollowedHashtags = ({ credentials, pagination: savedPagination }) => {
const queryParams = new URLSearchParams()
if (savedPagination?.maxId) {
queryParams.append('max_id', savedPagination.maxId)
}
const url = `${MASTODON_FOLLOWED_TAGS_URL}?${queryParams.toString()}`
let pagination = {};
return fetch(url, {
credentials
}).then((data) => {
pagination = parseLinkHeaderPagination(data.headers.get('Link'), {
flakeId: false
});
return data.json()
}).then((data) => {
return {
pagination,
data
}
});
}
const getFollowRequests = ({ credentials, pagination: savedPagination }) => {
const queryParams = new URLSearchParams()
if (savedPagination?.maxId) {
queryParams.append('max_id', savedPagination.maxId)
}
const url = `${MASTODON_FOLLOW_REQUESTS_URL}?${queryParams.toString()}`
let pagination = {};
return fetch(url, {
credentials
}).then((data) => {
pagination = parseLinkHeaderPagination(data.headers.get('Link'), { flakeId: true });
return data.json()
}).then((data) => {
return {
pagination,
data: data.map(parseUser)
}
});
}
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
return Object.entries({
...(credentials
@ -1802,6 +1738,7 @@ const apiService = {
mfaConfirmOTP,
addBackup,
listBackups,
fetchFollowRequests,
fetchLists,
createList,
getList,
@ -1847,12 +1784,7 @@ const apiService = {
getReports,
updateReportStates,
addNoteToReport,
deleteNoteFromReport,
getHashtag,
followHashtag,
unfollowHashtag,
getFollowedHashtags,
getFollowRequests
deleteNoteFromReport
}
export default apiService

View file

@ -1,6 +1,7 @@
import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js'
import timelineFetcher from '../timeline_fetcher/timeline_fetcher.service.js'
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
import listsFetcher from '../../services/lists_fetcher/lists_fetcher.service.js'
import announcementsFetcher from '../../services/announcements_fetcher/announcements_fetcher.service.js'
import configFetcher from '../config_fetcher/config_fetcher.service.js'
@ -27,6 +28,10 @@ const backendInteractorService = credentials => ({
return notificationsFetcher.fetchAndUpdate({ ...args, credentials })
},
startFetchingFollowRequests ({ store }) {
return followRequestFetcher.startFetching({ store, credentials })
},
startFetchingLists ({ store }) {
return listsFetcher.startFetching({ store, credentials })
},

View file

@ -68,15 +68,13 @@ export const parseUser = (data) => {
output.fields_html = data.fields.map(field => {
return {
name: escape(field.name),
value: field.value,
verified_at: field.verified_at
value: field.value
}
})
output.fields_text = data.fields.map(field => {
return {
name: unescape(field.name.replace(/<[^>]*>/g, '')),
value: unescape(field.value.replace(/<[^>]*>/g, '')),
verified_at: field.verified_at
value: unescape(field.value.replace(/<[^>]*>/g, ''))
}
})
@ -90,10 +88,8 @@ export const parseUser = (data) => {
output.friends_count = data.following_count
output.bot = data.bot
output.follow_requests_count = data.follow_requests_count
if (data.akkoma) {
output.instance = data.akkoma.instance
output.status_ttl_days = data.akkoma.status_ttl_days
}
if (data.pleroma) {
@ -235,14 +231,13 @@ export const parseAttachment = (data) => {
if (masto) {
// Not exactly same...
output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
output.meta = data.meta
output.meta = data.meta // not present in BE yet
output.id = data.id
} else {
output.mimetype = data.mimetype
// output.meta = ??? missing
}
output.blurhash = data.blurhash
output.url = data.url
output.large_thumb_url = data.preview_url
output.description = data.description
@ -412,10 +407,8 @@ export const parseNotification = (data) => {
if (masto) {
output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen
if (data.status) {
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
output.action = output.status // TODO: Refactor, this is unneeded
}
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
output.action = output.status // TODO: Refactor, this is unneeded
output.target = output.type !== 'move'
? null
: parseUser(data.target)

View file

@ -0,0 +1,23 @@
import apiService from '../api/api.service.js'
import { promiseInterval } from '../promise_interval/promise_interval.js'
const fetchAndUpdate = ({ store, credentials }) => {
return apiService.fetchFollowRequests({ credentials })
.then((requests) => {
store.commit('setFollowRequests', requests)
store.commit('addNewUsers', requests)
}, () => {})
.catch(() => {})
}
const startFetching = ({ credentials, store }) => {
const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store })
boundFetchAndUpdate()
return promiseInterval(boundFetchAndUpdate, 240000)
}
const followRequestFetcher = {
startFetching
}
export default followRequestFetcher

View file

@ -77,6 +77,9 @@ const getToken = ({ clientId, clientSecret, instance, code }) => {
body: form
})
.then((data) => data.json())
.catch((e) => {
console.error(e);
});
}
export const getClientToken = ({ clientId, clientSecret, instance }) => {

View file

@ -13,8 +13,7 @@ const postStatus = ({
quoteId = undefined,
contentType = 'text/plain',
preview = false,
idempotencyKey = '',
language
idempotencyKey = ''
}) => {
const mediaIds = map(media, 'id')
@ -30,8 +29,7 @@ const postStatus = ({
contentType,
poll,
preview,
idempotencyKey,
language
idempotencyKey
})
.then((data) => {
if (!data.error && !preview) {

View file

@ -4,10 +4,12 @@ import { getColors, computeDynamicColor, getOpacitySlot } from '../theme_data/th
export const applyTheme = (input) => {
const { rules } = generatePreset(input)
const head = document.head
const body = document.body
body.classList.add('hidden')
const styleEl = document.getElementById('theme-holder')
const styleEl = document.createElement('style')
head.appendChild(styleEl)
const styleSheet = styleEl.sheet
styleSheet.toString()

84
src/store.js Normal file
View file

@ -0,0 +1,84 @@
import { createStore } from 'vuex'
import 'custom-event-polyfill'
import './lib/event_target_polyfill.js'
import interfaceModule from './modules/interface.js'
import instanceModule from './modules/instance.js'
import statusesModule from './modules/statuses.js'
import listsModule from './modules/lists.js'
import usersModule from './modules/users.js'
import apiModule from './modules/api.js'
import configModule from './modules/config.js'
import serverSideConfigModule from './modules/serverSideConfig.js'
import oauthModule from './modules/oauth.js'
import authFlowModule from './modules/auth_flow.js'
import mediaViewerModule from './modules/media_viewer.js'
import oauthTokensModule from './modules/oauth_tokens.js'
import reportsModule from './modules/reports.js'
import pollsModule from './modules/polls.js'
import postStatusModule from './modules/postStatus.js'
import announcementsModule from './modules/announcements.js'
import editStatusModule from './modules/editStatus.js'
import statusHistoryModule from './modules/statusHistory.js'
import i18n from './i18n';
import createPersistedState from './lib/persisted_state.js'
import pushNotifications from './lib/push_notifications_plugin.js'
const persistedStateOptions = {
paths: [
'config',
'users.lastLoginName',
'oauth'
]
};
const getStore = async () => {
let storageError = false
const plugins = [pushNotifications]
try {
const persistedState = await createPersistedState(persistedStateOptions)
plugins.push(persistedState)
} catch (e) {
console.error(e)
storageError = true
}
const store = createStore({
modules: {
i18n: {
getters: {
i18n: () => i18n.global
}
},
interface: interfaceModule,
instance: instanceModule,
// TODO refactor users/statuses modules, they depend on each other
users: usersModule,
statuses: statusesModule,
lists: listsModule,
api: apiModule,
config: configModule,
serverSideConfig: serverSideConfigModule,
oauth: oauthModule,
authFlow: authFlowModule,
mediaViewer: mediaViewerModule,
oauthTokens: oauthTokensModule,
reports: reportsModule,
polls: pollsModule,
postStatus: postStatusModule,
announcements: announcementsModule,
editStatus: editStatusModule,
statusHistory: statusHistoryModule
},
plugins,
strict: false // Socket modifies itself, let's ignore this for now.
})
if (storageError) {
store.dispatch('pushGlobalNotice', { messageKey: 'errors.storage_unavailable', level: 'error' })
}
return store;
};
export default getStore;

45
static/.tos Normal file
View file

@ -0,0 +1,45 @@
<h4>Terms of Service</h4>
<p>It's mainly "be nice"</p>
<ol>
<li>
<h3>Don't be a big meanie</h4>
<p>Arguments are cool and all but don't make them into flamewars. Try to act in good faith - we want to be at least on good terms with people. Please act with understanding towards others on this instance. Most people here are probably struggling with a lot, be mindful of that.</p>
</li>
<li>
<h3>Mark your lewds!</h3>
<p>Reminder that lewd is bad and nobody wants to be forced to see that. Just mark it sensitive, and post unlisted. That is to say, anything suggestive/ecchi upwards should be marked. If you wouldn't look at it with your parents/boss in the room, mark it. It goes without saying that if you're <em>going</em> to post lewd stuff, keep it sensible. Obviously nothing underaged or otherwise questionable. Or you could just not post lewd stuff. Either/or.</p>
</li>
<li>
<h3>This is a <b>Kink Shame Zone</b></h3>
<p>Being a lewdie will be met with many anime girl reaction images shaming you for your lewdness. Go think about icky things on someone else's webzone™</p>
</li>
<li>
<h3>Keep it legal!</h3>
<p>Server is hosted in france, keep content legal for there (+ wherever you're browsing from)</p>
</li>
<li>
<h3>No ads/spambots</h3>
<p>I didn't think I'd have to specify this, but please do not set up bots solely for trying to advertise.</h3>
</li>
<li>
<h3>Non-TOS recommendations</h3>
<p>This is stuff that'd I'd <em>like</em> you to do, but I won't outright ban you if you don't follow them</p>
<ul>
<li>If someone is sadposting, don't antagonise them - they probably just want to vent</li>
<li>Put walls of text behind a subject (CW) - helps the timeline not get flooded with text</li>
</ul>
</li>
<li>
<h3>Other</h3>
<p>If you're here and you happen to play minecraft, feel free to message me with your username and come play with us sometime!</p>
</li>
</ol>
<p>So I guess yeah, that's about it. Try to be nice, eh? We're probably all sad here.</p>
<br>
<img src="/static/logo.png" style="display: block; margin: auto; max-width: 100%; height: 50px; object-fit: contain;" />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

View file

@ -1,4 +0,0 @@
/* THIS IS A PLACEHOLDER FILE
place a css file at $static_dir/static/custom.css
to apply custom styles to your frontend
*/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

103
static/logo.svg Executable file → Normal file
View file

@ -1,34 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 362.83 362.83">
<defs>
<style>
.cls-1 {
fill: #462d7a;
}
.cls-2 {
stroke: #2c1e50;
}
.cls-2, .cls-3 {
stroke-miterlimit: 10;
}
.cls-3 {
stroke: #fff;
}
</style>
</defs>
<g id="Layer_9" data-name="Layer 9">
<path class="cls-2" d="M269.3,197.19c-5.77-11.54-85.59,16.83-154.76,27.39-21.09,3.22-38.13,4.31-47.3,4.75-.74,2.91-1.76,7.02-2.87,11.97-1.93,8.6-2.89,12.89-2.6,13.78,3.3,9.95,59.73-.88,99.18-7.64,32.67-5.6,115.14-18.96,114.61-30.77-.03-.69-1.11-4.01-3.27-10.65-1.78-5.47-2.67-8.2-2.98-8.83Z"/>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg4485"
width="512"
height="512"
viewBox="0 0 512 512"
sodipodi:docname="logo.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
<metadata
id="metadata4491">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4489" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1274"
inkscape:window-height="1410"
id="namedview4487"
showgrid="false"
inkscape:zoom="1.2636719"
inkscape:cx="305.99333"
inkscape:cy="304.30809"
inkscape:window-x="1280"
inkscape:window-y="22"
inkscape:window-maximized="0"
inkscape:current-layer="g4612"
inkscape:document-rotation="0" />
<g
id="g4612">
<g
id="g850"
transform="matrix(0.99659595,0,0,0.99659595,0.37313949,0.87143746)">
<path
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#009bff;stroke-width:0;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.175879"
d="m 194.75841,124.65165 a 20.449443,20.449443 0 0 0 -20.44944,20.44945 v 242.24725 h 65.28091 v -262.6967 z"
id="path4497" />
<path
style="fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 272.6236,124.65165 V 256 h 45.61799 a 20.449443,20.449443 0 0 0 20.44944,-20.44945 v -110.8989 z"
id="path4516" />
<path
style="opacity:1;fill:#fba457;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 272.6236,322.06744 v 65.28091 h 45.61799 a 20.449443,20.449443 0 0 0 20.44944,-20.44945 v -44.83146 z"
id="path4516-5" />
</g>
</g>
<g id="Layer_6" data-name="Layer 6">
<path class="cls-1" d="M115.2,131.89c6.26-6.54,20.19-20.63,42.39-26.14,15.79-3.92,28.51-1.28,33.51,0,83.72,21.41,116.03,201.78,77.79,226.32-10.28,6.6-26.86,2.7-36.77-3.3-32.63-19.78-29.3-72.87-44.44-73.73-5.11-.29-7.15,5.8-20.91,24.94-19.63,27.3-31.49,43.44-49.21,50.87-2.53,1.06-26.91,12.07-41.84,1.23-38.55-28-2.96-155.84,39.49-200.18Zm56.31,10.45c-27.39-.52-46.38,38.21-37.98,54.55,10.09,19.62,65.5,18.26,74.77-3.3,7.21-16.78-11.38-50.77-36.79-51.24Z"/>
</g>
<g id="Layer_4" data-name="Layer 4">
<path d="M68.93,86.51c-6.55,27.74,252.45,113.97,267.56,89.66,9.24-14.87-64.9-83.62-163.53-97.57-39.06-5.52-100.95-5.14-104.03,7.91Z"/>
</g>
<g id="Layer_5" data-name="Layer 5">
<path class="cls-3" d="M138.96,93.76c.41-5.25,6.51-5.74,28.85-19.42,26.97-16.51,28.85-22.38,56.86-40.83,30.07-19.81,48.46-31.94,54.82-26.61,9.72,8.15-25.18,43.33-21.31,99.35,.87,12.61,3.12,17.79-.86,23.01-18.25,23.95-120.07-13.68-118.35-35.5Z"/>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -1 +0,0 @@
// This file intentionally left blank

667
yarn.lock

File diff suppressed because it is too large Load diff