Compare commits

..

No commits in common. "develop" and "develop" have entirely different histories.

111 changed files with 991 additions and 3008 deletions

1
.node-version Normal file
View file

@ -0,0 +1 @@
7.2.1

View file

@ -42,13 +42,10 @@ steps:
- develop
- stable
image: node:20
environment:
SCW_ACCESS_KEY:
from_secret: SCW_ACCESS_KEY
SCW_SECRET_KEY:
from_secret: SCW_SECRET_KEY
SCW_DEFAULT_ORGANIZATION_ID:
from_secret: SCW_DEFAULT_ORGANIZATION_ID
secrets:
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
- SCW_DEFAULT_ORGANIZATION_ID
commands:
- apt-get update && apt-get install -y rclone wget zip
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64
@ -67,13 +64,11 @@ steps:
- stable
environment:
CI: "true"
SCW_ACCESS_KEY:
from_secret: SCW_ACCESS_KEY
SCW_SECRET_KEY:
from_secret: SCW_SECRET_KEY
SCW_DEFAULT_ORGANIZATION_ID:
from_secret: SCW_DEFAULT_ORGANIZATION_ID
image: python:3.10-slim
secrets:
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
- SCW_DEFAULT_ORGANIZATION_ID
commands:
- apt-get update && apt-get install -y rclone wget git zip
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64

View file

@ -3,28 +3,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased (3.18)
### REMOVED
- dropped obsolete and buggy dm timeline
## Unreleased
### Added
- UI for conversations API, replacing the DM timeline.
Here each thread (conversation) has its own timeline and read markers instead of mixing everything together.
- Boosts now show when and with which visibility they were boosted
- bookmarks are now accessible via the narrow/mobile UI
### Fixed
- fixed saving fallback cop yof settings to local browser storage
- improve image animation detection further
- fix status content parsing for mention and hashtag detection; this could lock the UI until reload
- fix display of nsfw attachment overlays on webkit
## Between 2022.09 (3.2.0) and 2025.12 (3.17.0)
A whole lot of stuff, but we forgot to update the changelog besides the one entry below, oopsi
- Implemented remote interaction with statuses
## 2022.09 (3.2.0) - 2022-09-10
## 2022.09 - 2022-09-10
### Added
- Automatic post translations. Must be configured on the backend in order to work.
- Post editing, including a log of previous edits.

View file

@ -20,11 +20,9 @@ To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/ad
## Build Setup
Make sure you have [Node.js](https://nodejs.org/) installed. You can check `/.woodpecker.yml` for which node version the Akkoma CI currently uses.
``` bash
# install dependencies
npm install -g corepack
corepack enable
yarn
# serve with hot reload at localhost:8080
@ -55,4 +53,3 @@ 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.

View file

@ -15,13 +15,12 @@ put a file that looks like this
```json
{
"myPack": "/static/stickers/myPack/"
"myPack": "/static/stickers/myPack"
}
```
This file is a mapping from name to pack directory location. It says "we have a pack called myPack, look for
it inside `/static/stickers/myPack`". You can add as many packs as you like in this manner.
Note that a single leading and a trailing slash are **required** to work correctly!
it at `/static/stickers/myPack`". You can add as many packs as you like in this manner.
## Creating the pack

View file

@ -6,6 +6,7 @@
<title>Akkoma</title>
<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-->

View file

@ -1,6 +1,6 @@
{
"name": "pleroma_fe",
"version": "3.12.0",
"version": "3.10.0",
"description": "A frontend for Akkoma instances",
"author": "Roger Braun <roger@rogerbraun.net>",
"private": true,
@ -41,9 +41,9 @@
"qrcode": "^1.5.3",
"querystring-es3": "^0.2.1",
"url": "^0.11.3",
"vue": "^3.4.38",
"vue-i18n": "^9.14.0",
"vue-router": "^4.4.3",
"vue": "^3.2.31",
"vue-i18n": "^9.2.2",
"vue-router": "^4.3.2",
"vue-template-compiler": "^2.7.16",
"vuex": "^4.1.0"
},
@ -128,6 +128,5 @@
"engines": {
"node": ">= 16.0.0",
"npm": ">= 3.0.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
}

View file

@ -59,8 +59,7 @@ export default {
{
'-reverse': this.reverseLayout,
'-no-sticky-headers': this.noSticky,
'-has-new-post-button': this.newPostButtonShown,
'-wide-timeline': this.widenTimeline
'-has-new-post-button': this.newPostButtonShown
},
'-' + this.layoutType
]
@ -94,9 +93,6 @@ export default {
newPostButtonShown () {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
},
widenTimeline () {
return this.$store.getters.mergedConfig.widenTimeline
},
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
editingAvailable () { return this.$store.state.instance.editingAvailable },
layoutType () { return this.$store.state.interface.layoutType },

View file

@ -172,10 +172,6 @@ nav {
background-color: rgba(0, 0, 0, 0.15);
background-color: var(--underlay, rgba(0, 0, 0, 0.15));
z-index: -1000;
.-wide-timeline & {
margin:0 calc(var(--columnGap) / -2);
}
}
.app-layout {
@ -191,17 +187,12 @@ nav {
grid-template-rows: 1fr;
box-sizing: border-box;
margin: 0 auto;
padding: 0 calc(var(--columnGap) / 2);
align-content: flex-start;
flex-wrap: wrap;
justify-content: center;
min-height: 100vh;
overflow-x: clip;
&.-wide-timeline {
--maxiColumn: minmax(var(--miniColumn), 1fr);
}
.column {
--___columnMargin: var(--columnGap);

View file

@ -12,6 +12,7 @@ import routes from './routes'
import VBodyScrollLock from 'src/directives/body_scroll_lock'
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import { applyTheme } from '../services/style_setter/style_setter.js'
@ -182,12 +183,6 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('renderMisskeyMarkdown')
copyInstanceOption('sidebarRight')
if (config.backendCommitUrl)
copyInstanceOption('backendCommitUrl')
if (config.frontendCommitUrl)
copyInstanceOption('frontendCommitUrl')
return store.dispatch('setTheme', config['theme'])
}
@ -252,6 +247,17 @@ const getStickers = async ({ store }) => {
}
}
const getAppSecret = async ({ store }) => {
const { state, commit } = store
const { oauth, instance } = state
return getOrCreateApp({ ...oauth, instance: instance.server, commit })
.then((app) => getClientToken({ ...app, instance: instance.server }))
.then((token) => {
commit('setAppToken', token.access_token)
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
})
}
const resolveStaffAccounts = ({ store, accounts }) => {
const nicknames = accounts.map(uri => uri.split('/').pop())
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
@ -339,7 +345,7 @@ const setConfig = async ({ store }) => {
const apiConfig = configInfos[0]
const staticConfig = configInfos[1]
await setSettings({ store, apiConfig, staticConfig })
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
}
const checkOAuthToken = async ({ store }) => {

View file

@ -1,14 +1,12 @@
import PublicTimeline from 'components/public_timeline/public_timeline.vue'
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
import DMConvTimeline from 'components/dm_conv_timeline/dm_conv_timeline.vue'
import DMConvList from 'components/dm_conv_list/dm_conv_list.vue'
import DMConvRecipients from 'components/dm_conv_recipients/dm_conv_recipients.vue'
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
import BubbleTimeline from 'components/bubble_timeline/bubble_timeline.vue'
import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
import ConversationPage from 'components/conversation-page/conversation-page.vue'
import Interactions from 'components/interactions/interactions.vue'
import DMs from 'components/dm_timeline/dm_timeline.vue'
import UserProfile from 'components/user_profile/user_profile.vue'
import Search from 'components/search/search.vue'
import Registration from 'components/registration/registration.vue'
@ -49,9 +47,6 @@ export default (store) => {
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
{ name: 'bubble-timeline', path: '/main/bubble', component: BubbleTimeline },
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
{ name: 'dms', path: '/main/conversations', component: DMConvList, beforeEnter: validateAuthenticatedRoute },
{ name: 'dm_conversation', path: '/main/conversations/:id', component: DMConvTimeline, beforeEnter: validateAuthenticatedRoute },
{ name: 'dm-conversation-recipients', path: '/main/conversations/:id/recipients', component: DMConvRecipients },
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
@ -67,6 +62,7 @@ export default (store) => {
},
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile, meta: { dontScroll: true } },
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'registration', path: '/registration', component: Registration },
{ name: 'registration-request-sent', path: '/registration-request-sent', component: RegistrationRequestSent },
{ name: 'awaiting-email-confirmation', path: '/awaiting-email-confirmation', component: AwaitingEmailConfirmation },

View file

@ -19,17 +19,6 @@
height: 200px;
position: relative;
overflow: hidden;
align-content: center;
.status-popover & {
height: 200px;
}
}
&.-nsfw-placeholder {
.attachment-wrapper {
align-content: unset;
}
}
.description-container {
@ -126,24 +115,6 @@
align-items: center;
justify-content: center;
padding-top: 0.5em;
p {
line-height: 1.5;
padding: 0 0.5em;
white-space: pre-line;
text-align: center;
max-height: 200px;
overflow-y: auto;
scrollbar-color: var(--border) #0000;
width: 100%;
box-sizing: border-box;
.status-popover & {
text-overflow: ellipsis;
overflow: hidden;
height: 1lh;
}
}
}

View file

@ -267,11 +267,11 @@ const conversation = {
},
replies () {
let i = 1
return reduce(this.conversation, (result, { id, in_reply_to_status_id }) => {
const irid = in_reply_to_status_id
if (irid) {
result[irid] = result[irid] || []
result[irid].push({
@ -414,14 +414,6 @@ const conversation = {
},
toggleExpanded () {
this.expanded = !this.expanded
const navHeight = document.getElementById("nav").offsetHeight
const headingHeight = document.getElementsByClassName("timeline-heading")[0].offsetHeight
document.documentElement.style.setProperty("--timeline-scroll-margin-top", `${navHeight + headingHeight}px`)
this.$nextTick(() => {
if (!this.expanded) {
this.$el.scrollIntoView({ block: 'nearest' })
}
})
},
getConversationId (statusId) {
const status = this.$store.state.statuses.allStatusesObject[statusId]

View file

@ -278,7 +278,5 @@
&.-expanded.status-fadein {
margin: calc(var(--status-margin, $status-margin) / 2);
}
scroll-margin-block-start: var(--timeline-scroll-margin-top);
}
</style>

View file

@ -105,18 +105,6 @@
v-if="(currentUser || !privateMode) && showNavShortcuts"
class="nav-items right"
>
<router-link
v-if="currentUser"
class="nav-icon"
:to="{ name: 'dms' }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="envelope"
:title="$t('nav.dm_conversations')"
/>
</router-link>
<router-link
v-if="currentUser"
class="nav-icon"

View file

@ -1,80 +0,0 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
const DMConvCard = {
components: {
ConfirmModal,
Status,
UserAvatar
},
props: {
conversation: {
type: Object,
required: true
},
compact: {
type: Boolean,
default: true
},
showLastStatus: {
type: Boolean,
default: true
},
showFullControls: {
type: Boolean,
default: false
}
},
emits: ['deleted'],
data () {
return {
showingDeleteConfirmDialogue: false
}
},
computed: {
shouldConfirmDelete() {
return this.$store.getters.mergedConfig.modalOnDeleteDMConversation;
},
membersTruncated() {
// XXX: this should ideally adapt to panel width
const maxLen = 11
const full = this.conversation.accounts
const truncated = full.length > maxLen
const truncList = truncated ? full.slice(0, maxLen) : full
return {
truncated: truncated,
users: truncList
}
},
last_status_text() {
return this.conversation.last_status?.content
}
},
methods: {
markRead() {
this.$store.dispatch('markDMConversationAsRead', { id: this.conversation.id })
},
showDeleteConfirmModal() {
this.showingDeleteConfirmDialogue = true
},
hideDeleteConfirmModal() {
this.showingDeleteConfirmDialogue = false
},
deleteConversation() {
if (this.shouldConfirmDelete) {
this.showDeleteConfirmModal()
} else {
this.doDeleteConversation()
}
},
doDeleteConversation() {
this.$store.dispatch('deleteDMConversation', { id: this.conversation.id })
this.hideDeleteConfirmModal()
this.$emit('deleted')
}
}
}
export default DMConvCard

View file

@ -1,134 +0,0 @@
<template>
<div class="dm-conv-card">
<router-link
:to="{ name: 'dm_conversation', params: {id: conversation.id }}"
>
<div class="heading">
<div class="title-bar">
<div class="title-bar-left">
<div
v-if="conversation.unread"
class="unread"
>
<span
class="badge badge-notification"
role="figure"
:title="$t('dm_conv.unread_msg')"
:alt="$t('dm_conv.unread_msg')"
>
!
</span>
<button
class="button-unstyled"
:title="$t('dm_conv.mark_single_read_tooltip')"
@click.stop.prevent="markRead()"
>
<FAIcon
icon="check"
class="fa-scale-110 fa-old-padding dm-conv-mark-read"
/>
</button>
&nbsp;
</div>
<h4>{{ $t('dm_conv.default_name', {id: conversation.id}) }}</h4>
</div>
<div class="title-bar-right">
<button
class="button-unstyled button-delete"
:title="$t('dm_conv.delete_tooltip')"
@click.stop.prevent="deleteConversation()"
>
<FAIcon
icon="trash-alt"
class="fa-scale-110 fa-old-padding dm-conv-delete"
/>
</button>
</div>
</div>
</div>
<div class="members">
<UserAvatar
v-for="user in membersTruncated.users"
:key="user.id"
:user="user"
:compact="compact"
/>
<div
v-if="membersTruncated.truncated"
class="ellipsis"
>
...
</div>
</div>
</router-link>
<div
v-if="showLastStatus"
class="last-message"
>
<div class="last-message-title">
{{ $t('dm_conv.last_message_title') }}:
</div>
<Status
:statusoid="conversation.last_status"
:compact="true"
:is-preview="true"
/>
</div>
<div
v-if="showFullControls"
class="controls"
>
<button
class="btn button-default"
:title="$t('dm_conv.recipients_edit_mode_button_tooltip')"
@click.once="$router.push({ name: 'dm-conversation-recipients', params: { id: conversation.id }})"
>
{{ $t('dm_conv.recipients_edit_mode_button') }}
</button>
</div>
<teleport to="#modal">
<confirm-modal
v-if="showingDeleteConfirmDialogue"
:title="$t('dm_conv.delete_confirm_title')"
:confirm-text="$t('dm_conv.delete_confirm_accept_button')"
:cancel-text="$t('dm_conv.delete_confirm_cancel_button')"
@accepted="doDeleteConversation"
@cancelled="hideDeleteConfirmModal"
>
{{ $t('dm_conv.delete_confirm', { identifier: conversation.id }) }}
</confirm-modal>
</teleport>
</div>
</template>
<script src="./dm_conv_card.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.dm-conv-card {
.heading, .title-bar, .title-bar-left, .members {
display: flex;
flex-wrap: nowrap;
overflow-x: hidden;
}
.title-bar {
width: 100%;
justify-content: space-between;
}
.controls {
text-align: center;
}
.members {
padding: 6px 0;
}
.last-message-title {
font-style: italic;
color: var(--faint);
}
}
</style>

View file

@ -1,33 +0,0 @@
import DMConvCard from '../dm_conv_card/dm_conv_card.vue'
import List from '../list/list.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more'
const PaginatedDMConvList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchDMConversationList'),
select: (props, $store) => $store.state.dmConversations.allDMConversations || [],
destroy: (props, $store) => $store.dispatch('clearDMConversations'),
childPropName: 'items',
additionalPropNames: []
})(List)
const DMConvList = {
components: {
PaginatedDMConvList,
DMConvCard
},
data () {
return {}
},
computed: {
conversations() {
return this.$store.state.dmConversations.allDMConversations
}
},
methods: {
markAllRead() {
this.$store.dispatch('markAllDMConversationsAsRead')
}
}
}
export default DMConvList

View file

@ -1,47 +0,0 @@
<template>
<div class="settings panel panel-default dm-conv-panel">
<div class="panel-heading">
<div class="title">
{{ $t('nav.dm_conv_list') }}
</div>
</div>
<div class="panel-controls">
<button
class="btn button-default mark-all-read-button"
@click="markAllRead()"
>
{{ $t('dm_conv.mark_all_read_button') }}
</button>
</div>
<div class="panel-body dm-conv-list">
<PaginatedDMConvList>
<template #item="{item}">
<DMConvCard :conversation="item" />
</template>
</PaginatedDMConvList>
</div>
</div>
</template>
<script src="./dm_conv_list.js"></script>
<style lang="scss">
.dm-conv-panel {
.dm-conv-list {
margin: 0 1em;
.dm-conv-card {
margin: 2.5em 0;
}
}
.panel-controls {
margin-top: 0.5em;
text-align: center;
}
.mark-all-read-button {
display: inline-block;
}
}
</style>

View file

@ -1,57 +0,0 @@
import { mapGetters } from 'vuex'
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import ListUserSearch from '../list_user_search/list_user_search.vue'
const DMConvRecipients = {
data () {
return {
conversationId: null,
conversation: null,
recipients: [],
suggestions: []
}
},
components: {
BasicUserCard,
ListUserSearch
},
computed: {
conversationTitle () {
return this.$i18n.t('dm_conv.default_name', {id: this.conversationId})
},
...mapGetters(['findUser'])
},
methods: {
toggleUser (user) {
if (this.isRecipient(user)) {
this.recipients.filter((r) => r.id !== user.id)
} else {
this.recipients.push(user)
}
},
isRecipient (user) {
return this.recipients.some((r) => r.id == user.id)
},
onResults (results) {
this.suggestions = results.map((id) => this.findUser(id)).filter(user => user)
},
updateRecipients () {
const recipientIds = this.recipients.map((u) => u.id)
this.$store.dispatch('setDMConversationDetails', {id: this.conversationId, recipients: recipientIds })
.then((updateConv) => {
this.conversation = updateConv
this.recipients = updateConv.accounts
})
}
},
created () {
this.conversationId = this.$route.params.id
this.$store.dispatch('fetchDMConversationDetails', { id: this.conversationId })
.then((data) => {
this.conversation = data
this.recipients = data.accounts
})
},
}
export default DMConvRecipients

View file

@ -1,82 +0,0 @@
<template>
<div class="panel-default panel dm-conv-recipients-edit">
<div
ref="header"
class="panel-heading"
>
<div class="title">
{{ $t('dm_conv.recipients_edit_title', {conversation_name: conversationTitle}) }}
</div>
<button
class="btn button-default"
@click="$router.back"
>
{{ $t('nav.back') }}
</button>
</div>
<h4>
{{ $t('dm_conv.recipients_edit_current_title') }}
</h4>
<div class="member-list current-recipients">
<div
v-for="user in recipients"
:key="user.id"
class="member"
>
<BasicUserCard
:user="user"
class="selected"
@click.capture.prevent="toggleUser(user)"
/>
</div>
</div>
<h4>
{{ $t('dm_conv.recipients_edit_add_new_title') }}
</h4>
<ListUserSearch @results="onResults" />
<div class="member-list">
<div
v-for="user in suggestions"
:key="user.id"
class="member"
>
<BasicUserCard
:user="user"
:class="isRecipient(user) ? 'selected' : ''"
@click.capture.prevent="toggleUser(user)"
/>
</div>
</div>
<button
class="btn button-default"
@click="updateRecipients"
>
{{ $t('dm_conv.recipients_save') }}
</button>
</div>
</template>
<script src="./dm_conv_recipients.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.dm-conv-recipients-edit {
.member-list {
padding-bottom: 0.7rem;
}
.current-recipients {
margin-bottom: 1.5em;
}
.basic-user-card:hover,
.basic-user-card.selected {
cursor: pointer;
background-color: var(--selectedPost, $fallback--lightBg);
}
}
</style>

View file

@ -1,34 +0,0 @@
import DMConvCard from '../dm_conv_card/dm_conv_card.vue'
import Timeline from '../timeline/timeline.vue'
const DMConvTimeline = {
data () {
return {
conversationId: null
}
},
components: {
DMConvCard,
Timeline
},
computed: {
conversation () { return this.$store.getters.getDMConversationById(this.conversationId) },
timeline () { return this.$store.state.statuses.timelines.dmConv }
},
methods: {
forceLeave () {
this.$router.push('/')
}
},
created () {
this.conversationId = this.$route.params.id
this.$store.dispatch('fetchDMConversationDetails', { id: this.conversationId })
this.$store.dispatch('startFetchingTimeline', { timeline: 'dmConv', conversationId: this.conversationId })
},
unmounted () {
this.$store.dispatch('stopFetchingTimeline', 'dmConv')
this.$store.commit('clearTimeline', { timeline: 'dmConv' })
}
}
export default DMConvTimeline

View file

@ -1,24 +0,0 @@
<template>
<Timeline
title="$t('dm_conv.page_header')"
:timeline="timeline"
:conversation-id="conversationId"
timeline-name="dmConv"
>
<template
#extraHeading
>
<DMConvCard
v-if="conversation"
:conversation="conversation"
:compact="false"
:show-full-controls="true"
:show-last-status="false"
:link-to-timeline="false"
@deleted="forceLeave"
/>
</template>
</Timeline>
</template>
<script src="./dm_conv_timeline.js"></script>

View file

@ -0,0 +1,14 @@
import Timeline from '../timeline/timeline.vue'
const DMs = {
computed: {
timeline () {
return this.$store.state.statuses.timelines.dms
}
},
components: {
Timeline
}
}
export default DMs

View file

@ -0,0 +1,9 @@
<template>
<Timeline
:title="$t('nav.dms')"
:timeline="timeline"
:timeline-name="'dms'"
/>
</template>
<script src="./dm_timeline.js"></script>

View file

@ -1,5 +1,3 @@
import StillImage from '../still-image/still-image.vue'
const EMOJI_SIZE = 32 + 8
const GROUP_TITLE_HEIGHT = 24
const BUFFER_SIZE = 3 * EMOJI_SIZE
@ -19,9 +17,6 @@ const EmojiGrid = {
resizeObserver: null
}
},
components: {
StillImage
},
mounted () {
const rect = this.$refs.container.getBoundingClientRect()
this.containerWidth = rect.width

View file

@ -34,11 +34,10 @@
@click.stop.prevent="onEmoji(item.emoji)"
>
<span v-if="!item.emoji.imageUrl">{{ item.emoji.replacement }}</span>
<StillImage
<img
v-else
:src="item.emoji.imageUrl"
no-stop-gifs="true"
/>
>
</span>
</template>
</div>

View file

@ -1,6 +1,5 @@
import Completion from '../../services/completion/completion.js'
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import StillImage from '../still-image/still-image.vue'
import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
@ -121,8 +120,7 @@ const EmojiInput = {
}
},
components: {
EmojiPicker,
StillImage
EmojiPicker
},
computed: {
padEmoji () {

View file

@ -20,7 +20,6 @@
ref="picker"
show-keep-open
:class="{ hide: !showPicker }"
:visible="showPicker"
:enable-sticker-picker="enableStickerPicker"
class="emoji-picker-panel"
@emoji="insert"
@ -48,11 +47,10 @@
v-if="!suggestion.mfm"
class="image"
>
<StillImage
<img
v-if="suggestion.img"
:src="suggestion.img"
no-stop-gifs="true"
/>
>
<span v-else>{{ suggestion.replacement }}</span>
</span>
<div class="label">

View file

@ -1,4 +1,4 @@
const MFM_TAGS = ['bg', 'blur', 'bounce', 'center', 'fg', 'flip', 'font', 'jelly', 'jump', 'position', 'rainbow', 'rotate', 'scale', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow', 'rotate', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true }))
/**

View file

@ -1,7 +1,6 @@
import { defineAsyncComponent } from 'vue'
import Checkbox from '../checkbox/checkbox.vue'
import EmojiGrid from '../emoji_grid/emoji_grid.vue'
import StillImage from '../still-image/still-image.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faBoxOpen,
@ -27,17 +26,12 @@ const EmojiPicker = {
required: false,
type: Boolean,
default: false
},
visible: {
required: false,
type: Boolean,
default: true
}
},
data () {
return {
keyword: '',
activeGroup: this.getDefaultGroup(),
activeGroup: 'standard',
showingStickers: false,
keepOpen: false
}
@ -45,8 +39,7 @@ const EmojiPicker = {
components: {
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
Checkbox,
EmojiGrid,
StillImage
EmojiGrid
},
methods: {
debouncedSearch: debounce(function (e) {
@ -89,11 +82,6 @@ const EmojiPicker = {
return list.filter(emoji => {
return (regex.test(emoji.displayText) || (!emoji.imageUrl && emoji.replacement === this.keyword))
})
},
getDefaultGroup () {
if (!this.visible) return null
const recentEmojis = this.$store.getters.recentEmojis
return recentEmojis.length === 0 ? 'standard' : 'recent'
}
},
computed: {
@ -160,13 +148,6 @@ const EmojiPicker = {
stickerPickerEnabled () {
return (this.$store.state.instance.stickers || []).length !== 0 && this.enableStickerPicker
}
},
watch: {
visible (val, oldVal) {
if (val && this.activeGroup === null) {
this.activeGroup = this.getDefaultGroup()
}
}
}
}

View file

@ -18,11 +18,10 @@
@click.prevent="highlight(group.id)"
>
<span v-if="!group.first.imageUrl">{{ group.first.replacement }}</span>
<StillImage
<img
v-else
:src="group.first.imageUrl"
no-stop-gifs="true"
/>
>
</span>
<span
v-if="stickerPickerEnabled"

View file

@ -11,7 +11,7 @@
@click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()"
>
<template
<span
v-if="reaction.url !== null"
>
<StillImage
@ -19,15 +19,16 @@
:title="reaction.name"
:alt="reaction.name"
class="reaction-emoji"
height="2.55em"
/>
{{ reaction.count }}
</template>
<template v-else>
</span>
<span v-else>
<span class="reaction-emoji unicode-emoji">
{{ reaction.name }}
</span>
<span>{{ reaction.count }}</span>
</template>
</span>
</button>
</UserListPopover>
<a
@ -52,26 +53,23 @@
container-type: inline-size;
}
.unicode-emoji {
font-size: 210%;
}
.emoji-reaction {
padding: 2px 0.5em;
padding: 0 0.5em;
margin-right: 0.5em;
margin-top: 0.5em;
display: flex;
align-items: end;
align-items: center;
justify-content: center;
box-sizing: border-box;
.reaction-emoji {
width: auto;
max-width: 96cqw;
height: 2.55em !important;
margin-right: 0.25em;
&.still-image {
height: 2.55em;
}
&.unicode-emoji {
display: inline-block;
font-size: 2.125em; // assuming default line height of 1.2rem and emojis that don't exceed line height
line-height: 2.55rem;
}
}
&:focus {
outline: none;
@ -99,9 +97,9 @@
}
.button-default.picked-reaction {
&, &:hover {
box-shadow: inset 0 0 0 1px var(--accent, $fallback--link);
}
border: 1px solid var(--accent, $fallback--link);
margin-left: -1px; // offset the border, can't use inset shadows either
margin-right: calc(0.5em - 1px);
}
</style>

View file

@ -91,7 +91,7 @@ const ExtraButtons = {
.catch(err => this.$emit('onError', err.error.error))
},
copyLink () {
navigator.clipboard.writeText(this.status.canonical_id)
navigator.clipboard.writeText(this.statusLink)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
},
@ -187,6 +187,13 @@ const ExtraButtons = {
noTranslationTargetSet () {
return this.$store.getters.mergedConfig.translationLanguage === undefined
},
statusLink () {
if (this.status.is_local) {
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
} else {
return this.status.external_url
}
},
shouldConfirmDelete () {
return this.$store.getters.mergedConfig.modalOnDelete
},

View file

@ -88,8 +88,10 @@ const Gallery = {
set(this.sizes, id, { width, height })
},
rowStyle (row) {
if (!row.audio && !row.minimal && !row.grid) {
return { 'aspect-ratio': `1/${(1 / (row.items.length + 0.6))}` }
if (row.audio) {
return { 'padding-bottom': '25%' } // fixed reduced height for audio
} else if (!row.minimal && !row.grid) {
return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` }
}
},
itemStyle (id, row) {

View file

@ -96,15 +96,9 @@
.gallery-row {
position: relative;
height: 0;
width: 100%;
flex-grow: 1;
.Status & {
max-height: 30em;
}
&.-audio {
aspect-ratio: 4/1; // this is terrible, but it's how it was before so I'm not changing it >:(
}
&:not(:first-child) {
margin-top: 0.5em;

View file

@ -43,7 +43,7 @@ const LoginForm = {
}
oauthApi.getOrCreateApp(data)
.then((app) => { oauthApi.login({ ...data, ...app }) })
.then((app) => { oauthApi.login({ ...app, ...data }) })
},
submitPassword () {
const { clientId } = this.oauth

View file

@ -24,15 +24,14 @@
:min-scale="pinchZoomMinScale"
:reset-to-min-scale-limit="pinchZoomScaleResetLimit"
>
<StillImage
<img
:class="{ loading }"
class="modal-image"
:src="currentMedia.url"
:alt="currentMedia.description"
:title="currentMedia.description"
:image-load-handler="onImageLoaded"
no-stop-gifs="true"
/>
@load="onImageLoaded"
>
</PinchZoom>
</SwipeClick>
<VideoAttachment

View file

@ -42,14 +42,8 @@ const mediaUpload = {
.then((fileData) => {
self.$emit('uploaded', fileData)
self.decreaseUploadCount()
}, (error) => {
var msg = typeof error === "string" ? error : error.message
if (msg) {
self.$emit('upload-failed', 'message', [msg])
} else {
self.$emit('upload-failed', 'default')
}
console.warn(`Failed to upload media: ${error}`)
}, (error) => {
self.$emit('upload-failed', 'default')
self.decreaseUploadCount()
})
},

View file

@ -18,7 +18,6 @@
<input
id="code"
v-model="code"
autocomplete="one-time-code"
class="form-control"
>
</div>

View file

@ -53,9 +53,6 @@ const NavPanel = {
federating: state => state.instance.federating,
}),
...mapGetters(['unreadAnnouncementCount']),
unreadDMConversationsCount () {
return this.$store.state.users.currentUser?.pleroma?.unread_conversation_count || 0
},
followRequestCount () {
return this.$store.state.users.currentUser.follow_requests_count
}

View file

@ -25,24 +25,6 @@
<TimelineMenuContent class="timelines" />
</div>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'dms' }"
>
<FAIcon
fixed-width
class="fa-scale-110"
icon="envelope"
/>{{ $t("nav.dm_conversations") }}
<span
v-if="unreadDMConversationsCount > 0"
class="badge badge-notification"
>
{{ unreadDMConversationsCount }}
</span>
</router-link>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"

View file

@ -50,13 +50,6 @@ export default {
totalVotesCount () {
return this.poll.votes_count
},
totalFractionBase () {
// Due to a backend bug, we might not have any voter count info for remote polls
// in this case, fall back to count of votes even for multiple cjoice polls
// to be able to at least display _something_
const total_base = this.poll.multiple ? this.poll.voters_count : this.poll.votes_count
return total_base > 0 ? total_base : this.poll.votes_count
},
containerClass () {
return {
loading: this.loading
@ -77,11 +70,10 @@ export default {
},
methods: {
percentageForOption (count) {
const total = this.totalFractionBase
return total === 0 ? 0 : Math.round(count / total * 100)
return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100)
},
resultTitle (option) {
return `${option.votes_count}/${this.totalFractionBase} ${this.$t('polls.votes')}`
return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}`
},
fetchPoll () {
this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id })

View file

@ -24,7 +24,6 @@
<button
v-if="options.length > 2"
class="delete-option button-unstyled -hover-highlight"
type="button"
@click="deleteOption(index)"
>
<FAIcon icon="times" />
@ -33,7 +32,6 @@
<button
v-if="options.length < maxOptions"
class="add-option faint button-unstyled -hover-highlight"
type="button"
@click="addOption"
>
<FAIcon

View file

@ -10,7 +10,6 @@ import fileTypeService from '../../services/file_type/file_type.service.js'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { reject, map, uniqBy, debounce } from 'lodash'
import { usePostLanguageOptions } from 'src/lib/post_language'
import scopeUtils from 'src/lib/scope_utils.js'
import suggestor from '../emoji_input/suggestor.js'
import { mapGetters, mapState } from 'vuex'
import Checkbox from '../checkbox/checkbox.vue'
@ -86,7 +85,6 @@ const PostStatusForm = {
'quoteId',
'repliedUser',
'attentions',
'copyMessageLanguage',
'copyMessageScope',
'subject',
'disableSubject',
@ -150,12 +148,13 @@ const PostStatusForm = {
const preset = this.$route.query.message
let statusText = preset || ''
if (this.replyTo || this.quoteId || this.repliedUser) {
if (this.replyTo || this.quoteId) {
const currentUser = this.$store.state.users.currentUser
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
}
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, alwaysShowSubjectInput } = this.$store.getters.mergedConfig
const { postContentType: contentType, postLanguage: defaultPostLanguage, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage, alwaysShowSubjectInput } = this.$store.getters.mergedConfig
const postLanguage = defaultPostLanguage || interfaceToISOLanguage(interfaceLanguage)
let statusParams = {
spoilerText: this.subject || '',
@ -166,7 +165,7 @@ const PostStatusForm = {
poll: {},
mediaDescriptions: {},
visibility: this.suggestedVisibility(),
language: this.suggestedLanguage(),
language: postLanguage,
contentType
}
@ -181,7 +180,7 @@ const PostStatusForm = {
poll: this.statusPoll || {},
mediaDescriptions: this.statusMediaDescriptions || {},
visibility: this.statusScope || this.suggestedVisibility(),
language: this.statusLanguage || this.suggestedLanguage(),
language: this.statusLanguage || postLanguage,
contentType: statusContentType
}
}
@ -330,7 +329,6 @@ const PostStatusForm = {
watch: {
'newStatus': {
deep: true,
flush: 'sync',
handler () {
this.statusChanged()
}
@ -343,22 +341,17 @@ const PostStatusForm = {
this.saveDraft()
},
clearStatus () {
const config = this.$store.getters.mergedConfig
const newStatus = this.newStatus
this.newStatus = {
status: '',
spoilerText: '',
files: [],
nsfw: !!config.sensitiveByDefault,
visibility: this.suggestedVisibility(),
contentType: config.postContentType,
language: this.suggestedLanguage(),
visibility: newStatus.visibility,
contentType: newStatus.contentType,
language: newStatus.language,
poll: {},
mediaDescriptions: {}
}
const scopeselector = this.$refs.scopeselector
if (scopeselector) {
scopeselector.currentScope = this.newStatus.visibility
}
this.pollFormVisible = false
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
this.clearPollForm()
@ -518,7 +511,7 @@ const PostStatusForm = {
addMediaFile (fileInfo) {
this.newStatus.files.push(fileInfo)
if (this.$store.getters.mergedConfig.sensitiveIfSubject && this.newStatus.spoilerText !== '' || !!this.$store.getters.mergedConfig.sensitiveByDefault) {
if (this.$store.getters.mergedConfig.sensitiveIfSubject && this.newStatus.spoilerText !== '') {
this.newStatus.nsfw = true
}
this.$emit('resize', { delayed: true })
@ -767,19 +760,16 @@ const PostStatusForm = {
openProfileTab () {
this.$store.dispatch('openSettingsModalTab', 'profile')
},
suggestedLanguage () {
// Make sure the inherited language is actually valid
if (this.postLanguageOptions.find(o => o.value === this.copyMessageLanguage)) {
return this.copyMessageLanguage
}
const { postLanguage: defaultPostLanguage, interfaceLanguage } = this.$store.getters.mergedConfig
const postLanguage = defaultPostLanguage || interfaceToISOLanguage(interfaceLanguage)
return postLanguage
},
suggestedVisibility () {
const maxScope = this.copyMessageScope
const defaultScope = this.$store.state.users.currentUser.default_scope
return scopeUtils.negotiate(defaultScope, maxScope)
if (this.copyMessageScope) {
if (this.copyMessageScope === 'direct') {
return this.copyMessageScope
}
if (this.copyMessageScope !== 'public' && this.$store.state.users.currentUser.default_scope !== 'private') {
return this.copyMessageScope
}
}
return this.$store.state.users.currentUser.default_scope
}
}
}

View file

@ -18,7 +18,6 @@
>
<button
class="button-unstyled -link"
type="button"
@click="openProfileTab"
>
{{ $t('post_status.account_not_locked_warning_link') }}
@ -137,7 +136,6 @@
class="form-post-subject"
@input="onSubjectInput"
@focus="focusSubjectInput()"
@keydown.exact.enter.prevent
>
</EmojiInput>
<i18n-t
@ -196,7 +194,6 @@
>
<scope-selector
v-if="!disableVisibilitySelector"
ref="scopeselector"
:user-default="userDefaultScope"
:original-scope="copyMessageScope"
:initial-scope="newStatus.visibility"
@ -204,11 +201,10 @@
/>
<div
class="format-selector-container"
>
class="format-selector-container">
<div
class="format-selector"
>
>
<Select
id="post-language"
v-model="newStatus.language"
@ -276,7 +272,6 @@
<button
class="emoji-icon button-unstyled"
:title="$t('emoji.add_emoji')"
type="button"
@click="showEmojiPicker"
>
<FAIcon icon="smile-beam" />
@ -286,7 +281,6 @@
class="poll-icon button-unstyled"
:class="{ selected: pollFormVisible }"
:title="$t('polls.add_poll')"
type="button"
@click="togglePollForm"
>
<FAIcon icon="poll-h" />
@ -296,7 +290,6 @@
class="spoiler-icon button-unstyled"
:class="{ selected: subjectVisible }"
:title="$t('post_status.toggle_content_warning')"
type="button"
@click="toggleSubjectVisible"
>
<FAIcon icon="eye-slash" />

View file

@ -7,16 +7,8 @@ const QuoteButton = {
name: 'QuoteButton',
props: ['status', 'quoting', 'visibility'],
computed: {
showButton () {
const currentUserId = this.$store.state.users.currentUser?.id
if (!currentUserId)
return false
if (['public', 'unlisted', 'local'].includes(this.visibility))
return true
return (this.visibility === 'private' && currentUserId == this.status.user.id)
loggedIn () {
return !!this.$store.state.users.currentUser
}
}
}

View file

@ -1,6 +1,6 @@
<template>
<div
v-if="showButton"
v-if="(visibility === 'public' || visibility === 'unlisted') && loggedIn"
class="QuoteButton"
>
<button

View file

@ -1,6 +1,4 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import ScopeSelector from '../scope_selector/scope_selector.vue'
import scopeUtils from 'src/lib/scope_utils.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faRetweet } from '@fortawesome/free-solid-svg-icons'
@ -9,16 +7,12 @@ library.add(faRetweet)
const RetweetButton = {
props: ['status', 'loggedIn', 'visibility'],
components: {
ConfirmModal,
ScopeSelector
ConfirmModal
},
data () {
const maxScope = this.status.visibility
const defaultScope = this.$store.state.users.currentUser.default_scope
return {
animated: false,
showingConfirmDialog: false,
retweetVisibility: scopeUtils.negotiate(defaultScope, maxScope)
showingConfirmDialog: false
}
},
methods: {
@ -31,7 +25,7 @@ const RetweetButton = {
},
doRetweet () {
if (!this.status.repeated) {
this.$store.dispatch('retweet', { id: this.status.id, visibility: this.retweetVisibility })
this.$store.dispatch('retweet', { id: this.status.id })
} else {
this.$store.dispatch('unretweet', { id: this.status.id })
}
@ -46,9 +40,6 @@ const RetweetButton = {
},
hideConfirmDialog () {
this.showingConfirmDialog = false
},
changeVis (visibility) {
this.retweetVisibility = visibility
}
},
computed: {
@ -63,13 +54,7 @@ const RetweetButton = {
},
remoteInteractionLink () {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
},
userDefaultScope () {
return this.$store.state.users.currentUser.default_scope
},
statusScope () {
return this.status.visibility
},
}
}
}

View file

@ -49,12 +49,6 @@
@cancelled="hideConfirmDialog"
>
{{ $t('status.repeat_confirm') }}
<scope-selector
:user-default="userDefaultScope"
:original-scope="statusScope"
:initial-scope="retweetVisibility"
:on-scope-change="changeVis"
/>
</confirm-modal>
</teleport>
</div>

View file

@ -121,19 +121,6 @@ export default {
}
}
const mfmStyleFromDataAttributes = (attributes) => {
// CSS selectors can check if a data-* attribute is true, but can't use other values, so we want to add them to the style attribute
// Here we turn e.g. `{'data-mfm-some': '1deg', 'data-mfm-thing': '5s'}` to "--mfm-some: 1deg;--mfm-thing: 5s;"
// Note that we only add the value to `style` when they contain only letters, numbers, dot, or minus signs
// At the moment of writing, this should be enough for legitimate purposes and reduces the chance of injection by using special characters
// There is a special case for the `color` value, who is provided without `#`, but requires this in the `style` attribute
return Object.keys(attributes).filter(
(key) => key.startsWith('data-mfm-') && attributes[key] !== true && /^[a-zA-Z0-9.\-]*$/.test(attributes[key])
).map(
(key) => '--mfm-' + key.substr(9) + (key === 'data-mfm-color' ? ': #' : ': ') + attributes[key] + ';'
).reduce((a,v) => a+v, '')
}
// Processor to use with html_tree_converter
const processItem = (item, index, array, what) => {
// Handle text nodes - just add emoji
@ -204,15 +191,6 @@ export default {
if (this.handleLinks && attrs?.['class']?.includes?.('h-card')) {
return ['', children.map(processItem), '']
}
let mfm_style = mfmStyleFromDataAttributes(attrs)
if (mfm_style !== '') {
return [
opener.slice(0,-1) + ' style="' + mfm_style + '">',
children.map(processItem),
closer
]
}
}
if (children !== undefined) {

View file

@ -6,8 +6,6 @@ import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
import scopeUtils from 'src/lib/scope_utils.js'
library.add(
faEnvelope,
faGlobe,
@ -15,11 +13,18 @@ library.add(
faLockOpen
)
const SCOPE_LEVELS = {
'direct': 0,
'private': 1,
'local': 2,
'unlisted': 2,
'public': 3
}
const ScopeSelector = {
props: [
'showAll',
'userDefault',
// scope of parent object
'originalScope',
'initialScope',
'onScopeChange'
@ -34,16 +39,16 @@ const ScopeSelector = {
return !this.showPublic && !this.showUnlisted && !this.showPrivate && !this.showDirect
},
showPublic () {
return this.shouldShow('public')
return this.originalScope !== 'direct' && this.shouldShow('public')
},
showLocal () {
return this.shouldShow('local')
return this.originalScope !== 'direct' && this.shouldShow('local')
},
showUnlisted () {
return this.shouldShow('unlisted')
return this.originalScope !== 'direct' && this.shouldShow('unlisted')
},
showPrivate () {
return this.shouldShow('private')
return this.originalScope !== 'direct' && this.shouldShow('private')
},
showDirect () {
return this.shouldShow('direct')
@ -60,10 +65,15 @@ const ScopeSelector = {
},
methods: {
shouldShow (scope) {
if (!this.originalScope)
if (!this.originalScope) {
return true
else
return scopeUtils.isSubScope(this.originalScope, scope)
}
if (this.originalScope === 'local') {
return scope === 'direct' || scope === 'local'
}
return SCOPE_LEVELS[scope] <= SCOPE_LEVELS[this.originalScope]
},
changeVis (scope) {
this.currentScope = scope

View file

@ -69,7 +69,7 @@ const SettingsModal = {
this.$store.dispatch('closeSettingsModal')
},
logout () {
this.$router.replace(this.$store.state.instance.redirectRootNoLogin || '/main/all')
this.$router.replace('/main/public')
this.$store.dispatch('closeSettingsModal')
this.$store.dispatch('logout')
},

View file

@ -159,16 +159,6 @@
{{ $t('settings.show_page_backgrounds') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="centerAlignBio">
{{ $t('settings.center_align_bio') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="compactUserInfo">
{{ $t('settings.compact_user_info') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="stopGifs">
{{ $t('settings.stop_gifs') }}
@ -279,11 +269,6 @@
{{ $t('settings.right_sidebar') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="widenTimeline">
{{ $t('settings.widen_timeline') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
v-if="user"
@ -335,11 +320,6 @@
{{ $t('settings.confirm_dialogs_deny_follow') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnDeleteDMConversation">
{{ $t('settings.confirm_dialogs_delete_dm_conv') }}
</BooleanSetting>
</li>
</ul>
</li>
</ul>

View file

@ -1,25 +1,22 @@
import { extractCommit } from 'src/services/version/version.service'
function joinURL(base, subpath) {
return URL.parse(subpath, base)?.href || "invalid base URL"
}
const pleromaFeCommitUrl = 'https://akkoma.dev/AkkomaGang/pleroma-fe/commit/'
const pleromaBeCommitUrl = 'https://akkoma.dev/AkkomaGang/akkoma/commit/'
const VersionTab = {
data () {
const instance = this.$store.state.instance
return {
backendCommitUrl: instance.backendCommitUrl,
backendVersion: instance.backendVersion,
frontendCommitUrl: instance.frontendCommitUrl,
frontendVersion: instance.frontendVersion
}
},
computed: {
frontendVersionLink () {
return joinURL(this.frontendCommitUrl, this.frontendVersion)
return pleromaFeCommitUrl + this.frontendVersion
},
backendVersionLink () {
return joinURL(this.backendCommitUrl, extractCommit(this.backendVersion))
return pleromaBeCommitUrl + extractCommit(this.backendVersion)
}
}
}

View file

@ -9,7 +9,6 @@ import {
faHome,
faComments,
faBolt,
faBookmark,
faUserPlus,
faBullhorn,
faSearch,
@ -26,7 +25,6 @@ library.add(
faHome,
faComments,
faBolt,
faBookmark,
faUserPlus,
faBullhorn,
faSearch,
@ -45,6 +43,10 @@ const SideDrawer = {
}),
created () {
this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer)
if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequests')
}
},
components: { UserCard },
computed: {
@ -57,9 +59,6 @@ const SideDrawer = {
unseenNotificationsCount () {
return this.unseenNotifications.length
},
unreadDMConversationsCount () {
return this.$store.state.users.currentUser?.pleroma?.unread_conversation_count || 0
},
suggestionsEnabled () {
return this.$store.state.instance.suggestionsEnabled
},

View file

@ -55,24 +55,6 @@
/> {{ $t("nav.timelines") }}
</router-link>
</li>
<li
v-if="currentUser"
@click="toggleDrawer"
>
<router-link :to="{ name: 'dms' }">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="envelope"
/> {{ $t("nav.dm_conversations") }}
<span
v-if="unreadDMConversationsCount > 0"
class="badge badge-notification"
>
{{ unreadDMConversationsCount }}
</span>
</router-link>
</li>
<li
v-if="currentUser"
@click="toggleDrawer"
@ -85,18 +67,6 @@
/> {{ $t("nav.lists") }}
</router-link>
</li>
<li
v-if="currentUser"
@click="toggleDrawer"
>
<router-link :to="{ name: 'bookmarks' }">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="bookmark"
/> {{ $t("nav.bookmarks") }}
</router-link>
</li>
</ul>
<ul v-if="currentUser">
<li @click="toggleDrawer">

View file

@ -169,8 +169,8 @@ const Status = {
},
computed: {
...controlledOrUncontrolledGetters(['replying', 'quoting', 'mediaPlaying']),
muteWordRules () {
return this.$store.getters.parsedConfigVal('muteWords')
muteWords () {
return this.mergedConfig.muteWords
},
showReasonMutedThread () {
return (
@ -215,7 +215,6 @@ const Status = {
retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name_ui },
retweeterHtml () { return this.statusoid.user.name },
retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },
retweeterVisibility () { return this.statusoid.visibility },
status () {
if (this.retweet) {
return this.statusoid.retweeted_status
@ -231,7 +230,7 @@ const Status = {
return !!this.currentUser
},
muteWordHits () {
return muteWordHits(this.status, this.muteWordRules)
return muteWordHits(this.status, this.muteWords)
},
rtBotStatus () {
return this.statusoid.user.bot
@ -441,9 +440,6 @@ const Status = {
visibilityLocalized () {
return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
},
retweeterVisibilityLocalized () {
return this.$i18n.t('general.scope_in_timeline.' + this.statusoid.visibility)
},
isEdited () {
return this.status.edited_at !== null
},

View file

@ -99,43 +99,20 @@
<router-link
v-else
:to="retweeterProfileLink"
>
{{ retweeter }}
</router-link>
>{{ retweeter }}</router-link>
</div>
{{ ' ' }}
<div
class="repeat-tooltip"
>
<FAIcon
icon="retweet"
class="repeat-icon"
:title="$t('tool_tip.repeat')"
/>
{{ $t('timeline.repeated') }}
<FAIcon
icon="retweet"
class="repeat-icon"
:title="$t('tool_tip.repeat')"
/>
{{ $t('timeline.repeated') }}
</div>
<span
v-if="retweeterVisibility"
class="visibility-icon"
:title="retweeterVisibilityLocalized"
>
<FAIcon
fixed-width
class="fa-scale-110"
:icon="visibilityIcon(retweeterVisibility)"
/>
</span>
{{ ' ' }}
<Timeago
class="timeago"
:time="statusoid.created_at"
:with-direction="!compact"
:auto-update="60"
/>
</div>
</div>
@ -542,7 +519,6 @@
:reply-to="status.id"
:attentions="status.attentions"
:replied-user="status.user"
:copy-message-language="status.language"
:copy-message-scope="status.visibility"
:subject="replySubject"
@posted="toggleReplying"
@ -557,7 +533,6 @@
:quote-id="status.id"
:attentions="[status.user]"
:replied-user="status.user"
:copy-message-language="status.language"
:copy-message-scope="status.visibility"
:subject="replySubject"
@posted="toggleQuoting"

View file

@ -41,8 +41,7 @@ const StatusContent = {
postLength: this.status.text.length,
parseReadyDone: false,
renderMisskeyMarkdown,
translateFrom: null,
translating: false
translateFrom: null
}
},
computed: {
@ -136,10 +135,7 @@ const StatusContent = {
},
translateStatus () {
const translateTo = this.$store.getters.mergedConfig.translationLanguage || this.$store.state.instance.interfaceLanguage
this.translating = true
this.$store.dispatch(
'translateStatus', { id: this.status.id, language: translateTo, from: this.translateFrom }
).finally(() => { this.translating = false })
this.$store.dispatch('translateStatus', { id: this.status.id, language: translateTo, from: this.translateFrom })
}
}
}

View file

@ -3,7 +3,6 @@
.StatusBody {
display: flex;
flex-direction: column;
overflow: hidden;
.translation {
border: 1px solid var(--accent, $fallback--link);
@ -24,6 +23,24 @@
transition: 0.05s;
}
._mfm_x2_ {
.emoji {
height: 100px;
}
}
._mfm_x3_ {
.emoji {
height: 150px;
}
}
._mfm_x4_ {
.emoji {
height: 200px;
}
}
.attachments {
margin-top: 0.5em;
}

View file

@ -1,7 +1,7 @@
<template>
<div
class="StatusBody"
:class="{ '-compact': compact }"
:class="{ '-compact': compact, 'mfm-disabled': !renderMisskeyMarkdown }"
>
<div class="body">
<div
@ -91,7 +91,6 @@
{{ ' ' }}
<button
class="btn button-default"
:disabled="translating"
@click="translateStatus"
>
{{ $t('status.translate') }}

View file

@ -1,423 +0,0 @@
/**
* "FEP-c16b: Formatting MFM functions" attributes that Akkoma supports
*/
.StatusContent:not(.mfm-disabled) {
/* The following are the non-animated MFM */
.mfm-center {
display: block;
text-align: center;
}
.mfm-flip {
display: inline-block;
transform: scaleX(-1);
}
.mfm-flip[data-mfm-v] {
transform: scaleY(-1);
}
.mfm-flip[data-mfm-v][data-mfm-h] {
transform: scale(-1, -1);
}
.mfm-font[data-mfm-serif] {
font-family: serif;
}
.mfm-font[data-mfm-monospace] {
font-family: monospace;
}
.mfm-font[data-mfm-cursive] {
font-family: cursive;
}
.mfm-font[data-mfm-fantasy] {
font-family: fantasy;
}
.mfm-font[data-mfm-emoji] {
font-family: emoji;
}
.mfm-font[data-mfm-math] {
font-family: math;
}
.mfm-blur {
filter: blur(6px);
transition: filter 0.3s;
&:hover {
filter: blur(0);
}
}
.mfm-rotate {
display: inline-block;
transform: rotate(calc(var(--mfm-deg, 90) * 1deg));
transform-origin: center center;
}
.mfm-x2 {
--mfm-zoom-size: 200%;
}
.mfm-x3 {
--mfm-zoom-size: 400%;
}
.mfm-x4 {
--mfm-zoom-size: 600%;
}
.mfm-x2,
.mfm-x3,
.mfm-x4,
.mfm-tada {
.emoji {
--emoji-size: 2em;
}
font-size: var(--mfm-zoom-size);
.mfm-x2,
.mfm-x3,
.mfm-x4,
.mfm-tada {
/* only half effective */
font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
.mfm-x2,
.mfm-x3,
.mfm-x4,
.mfm-tada {
/* disabled */
font-size: 100%;
}
}
}
.mfm-position {
display: inline-block;
transform: translate(calc(var(--mfm-x, 0) * 1em), calc(var(--mfm-y, 0) * 1em));
}
.mfm-scale {
display: inline-block;
transform: scale(var(--mfm-x, 1), var(--mfm-y, 1));
}
.mfm-fg {
color: var(--mfm-color, #f00);
}
.mfm-bg {
background-color: var(--mfm-color, #0f0);
}
/* The following are the animated MFM */
/* .mfm-hover means that we should only play animation when hovering over the StatusContent
* So either StatusContent does not have this class,
* or it has the class and we are hovering over StatusContent
*/
&:not(.mfm-hover:not(:hover)) {
.mfm-jelly {
display: inline-block;
animation: mfm-rubberBand var(--mfm-speed, 1s) linear infinite both;
}
.mfm-twitch {
display: inline-block;
animation: mfm-twitch var(--mfm-speed, 0.5s) ease infinite;
}
.mfm-shake {
display: inline-block;
animation: mfm-shake var(--mfm-speed, 0.5s) ease infinite;
}
.mfm-spin {
display: inline-block;
animation: mfm-spin var(--mfm-speed, 1.5s) linear infinite;
}
.mfm-spin[data-mfm-y] {
animation-name: mfm-spinY;
}
.mfm-spin[data-mfm-x] {
animation-name: mfm-spinX;
}
.mfm-spin[data-mfm-alternate] {
animation-direction: alternate;
}
.mfm-spin[data-mfm-left] {
animation-direction: reverse;
}
.mfm-jump {
display: inline-block;
animation: mfm-jump var(--mfm-speed, 0.75s) linear infinite;
}
.mfm-bounce {
display: inline-block;
animation: mfm-bounce var(--mfm-speed, 0.75s) linear infinite;
transform-origin: center bottom;
}
.mfm-rainbow {
animation: mfm-rainbow var(--mfm-speed, 1s) linear infinite;
}
.mfm-tada {
display: inline-block;
animation: mfm-tada var(--mfm-speed, 1s) linear infinite both;
--mfm-zoom-size: 150%;
}
}
/* animation keyframes */
@keyframes mfm-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes mfm-spinX {
0% { transform: perspective(128px) rotateX(0deg); }
100% { transform: perspective(128px) rotateX(360deg); }
}
@keyframes mfm-spinY {
0% { transform: perspective(128px) rotateY(0deg); }
100% { transform: perspective(128px) rotateY(360deg); }
}
@keyframes mfm-jump {
0% { transform: translateY(0); }
25% { transform: translateY(-16px); }
50% { transform: translateY(0); }
75% { transform: translateY(-8px); }
100% { transform: translateY(0); }
}
@keyframes mfm-bounce {
0% { transform: translateY(0) scale(1, 1); }
25% { transform: translateY(-16px) scale(1, 1); }
50% { transform: translateY(0) scale(1, 1); }
75% { transform: translateY(0) scale(1.5, 0.75); }
100% { transform: translateY(0) scale(1, 1); }
}
@keyframes mfm-twitch {
0% { transform: translate(7px, -2px); }
5% { transform: translate(-3px, 1px); }
10% { transform: translate(-7px, -1px); }
15% { transform: translate(0, -1px); }
20% { transform: translate(-8px, 6px); }
25% { transform: translate(-4px, -3px); }
30% { transform: translate(-4px, -6px); }
35% { transform: translate(-8px, -8px); }
40% { transform: translate(4px, 6px); }
45% { transform: translate(-3px, 1px); }
50% { transform: translate(2px, -10px); }
55% { transform: translate(-7px, 0); }
60% { transform: translate(-2px, 4px); }
65% { transform: translate(3px, -8px); }
70% { transform: translate(6px, 7px); }
75% { transform: translate(-7px, -2px); }
80% { transform: translate(-7px, -8px); }
85% { transform: translate(9px, 3px); }
90% { transform: translate(-3px, -2px); }
95% { transform: translate(-10px, 2px); }
100% { transform: translate(-2px, -6px); }
}
@keyframes mfm-shake {
0% { transform: translate(-3px, -1px) rotate(-8deg); }
5% { transform: translate(0, -1px) rotate(-10deg); }
10% { transform: translate(1px, -3px) rotate(0deg); }
15% { transform: translate(1px, 1px) rotate(11deg); }
20% { transform: translate(-2px, 1px) rotate(1deg); }
25% { transform: translate(-1px, -2px) rotate(-2deg); }
30% { transform: translate(-1px, 2px) rotate(-3deg); }
35% { transform: translate(2px, 1px) rotate(6deg); }
40% { transform: translate(-2px, -3px) rotate(-9deg); }
45% { transform: translate(0, -1px) rotate(-12deg); }
50% { transform: translate(1px, 2px) rotate(10deg); }
55% { transform: translate(0, -3px) rotate(8deg); }
60% { transform: translate(1px, -1px) rotate(8deg); }
65% { transform: translate(0, -1px) rotate(-7deg); }
70% { transform: translate(-1px, -3px) rotate(6deg); }
75% { transform: translate(0, -2px) rotate(4deg); }
80% { transform: translate(-2px, -1px) rotate(3deg); }
85% { transform: translate(1px, -3px) rotate(-10deg); }
90% { transform: translate(1px, 0) rotate(3deg); }
95% { transform: translate(-2px, 0) rotate(-3deg); }
100% { transform: translate(2px, 1px) rotate(2deg); }
}
@keyframes mfm-rubberBand {
0% { transform: scale3d(1, 1, 1); }
30% { transform: scale3d(1.25, 0.75, 1); }
40% { transform: scale3d(0.75, 1.25, 1); }
50% { transform: scale3d(1.15, 0.85, 1); }
65% { transform: scale3d(0.95, 1.05, 1); }
75% { transform: scale3d(1.05, 0.95, 1); }
100% { transform: scale3d(1, 1, 1); }
}
@keyframes mfm-rainbow {
0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
}
@keyframes mfm-tada {
0%,
100% { transform: scale3d(1, 1, 1); }
10%,
20% { transform: scale3d(0.9, 0.9, 0.9) rotate3d(0, 0, 1, -3deg); }
30%,
50%,
70%,
90% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, 3deg); }
40%,
60%,
80% { transform: scale3d(1.1, 1.1, 1.1) rotate3d(0, 0, 1, -3deg); }
}
/**
* Legacy MFM
* This is for backwards compatibility with posts formatted on Akkoma before support for FEP-c16b
* Note that it uses the keyframes as defined above for the FEP-c16b compatible MFM representation
*/
.mfm {
display: inline-block;
}
/* The following are the legacy non-animated MFM */
._mfm_flip_[data-h][data-v] {
transform: scale(-1, -1);
}
._mfm_flip_[data-v] {
transform: scaleY(-1);
}
._mfm_flip_:not([data-v]) {
transform: scaleX(-1);
}
._mfm_x2_ {
font-size: 200%;
}
._mfm_x3_ {
font-size: 400%;
}
._mfm_x4_ {
font-size: 600%;
}
._mfm_x2_ {
.emoji {
height: 100px;
}
}
._mfm_x3_ {
.emoji {
height: 150px;
}
}
._mfm_x4_ {
.emoji {
height: 200px;
}
}
._mfm_blur_ {
filter: blur(6px);
transition: filter 0.3s;
}
._mfm_blur_:hover {
filter: blur(0);
}
._mfm_rotate_ {
transform: rotate(90deg);
transform-origin: center center;
}
/* The following are the legacy animated MFM */
/* .mfm-hover means that we should only play animation when hovering over the StatusContent
* So either StatusContent does not have this class,
* or it has the class and we are hovering over StatusContent
*/
&:not(.mfm-hover:not(:hover)) {
._mfm_tada_ {
font-size: 150%;
animation: mfm-tada 1s linear infinite both;
}
._mfm_jelly_ {
animation: mfm-rubberBand 1s linear infinite both;
}
._mfm_twitch_ {
animation: mfm-twitch 0.5s ease infinite;
}
._mfm_shake_ {
animation: mfm-shake 0.5s ease infinite;
}
._mfm_spin_ {
animation: mfm-spin 0.5s linear infinite;
}
._mfm_spin_[data-x] {
animation-name: mfm-spinX;
}
._mfm_spin_[data-y] {
animation-name: mfm-spinY;
}
._mfm_spin_[left] {
animation-direction: reverse;
}
._mfm_spin_[alternate] {
animation-direction: alternate;
}
._mfm_jump_ {
animation: mfm-jump 0.75s linear infinite;
}
._mfm_bounce_ {
animation: mfm-bounce 0.75s linear infinite;
transform-origin: center bottom;
}
._mfm_rainbow_ {
animation: mfm-rainbow 1s linear infinite;
}
}
}

View file

@ -106,9 +106,6 @@ const StatusContent = {
renderMisskeyMarkdown () {
return this.mergedConfig.renderMisskeyMarkdown
},
hasResolvedQuote () {
return !!this.status.quote
},
...mapGetters(['mergedConfig']),
...mapState({
currentUser: state => state.users.currentUser

View file

@ -1,7 +1,7 @@
<template>
<div
class="StatusContent"
:class="{ '-compact': compact, 'mfm-hover': renderMfmOnHover, 'mfm-disabled': !renderMisskeyMarkdown, 'quote-resolved': hasResolvedQuote }"
:class="{ '-compact': compact, 'mfm-hover': renderMfmOnHover, 'mfm-disabled': !renderMisskeyMarkdown }"
>
<slot name="header" />
<StatusBody
@ -64,7 +64,6 @@
</template>
<script src="./status_content.js"></script>
<style lang="scss" src="./mfm.scss" />
<style lang="scss">
.StatusContent {
flex: 1;
@ -76,15 +75,28 @@
height: 50px;
}
}
&.mfm-hover:not(:hover) {
.mfm {
animation: none !important;
}
}
&.mfm-disabled {
span {
font-size: 100% !important;
}
.mfm {
animation: none !important;
}
.emoji {
height: 32px !important;
}
}
}
.quote-resolved .quote-inline,
.quote-inline,
.quote + .link-preview {
display: none;
}
.quote-resolved .quote .quote-inline {
display: inline;
display: none;
}
</style>

View file

@ -7,19 +7,12 @@ const StillImage = {
'imageLoadHandler',
'alt',
'height',
'width',
'noStopGifs'
'width'
],
data () {
return {
stopGifs:
!this.noStopGifs
&& (
this.$store.getters.mergedConfig.stopGifs
|| window.matchMedia('(prefers-reduced-motion: reduce)').matches
),
stopGifs: this.$store.getters.mergedConfig.stopGifs || window.matchMedia('(prefers-reduced-motion: reduce)').matches,
isAnimated: false,
isPixelArt: false,
imageTypeLabel: ''
}
},
@ -41,18 +34,11 @@ const StillImage = {
if (!image) return
this.imageLoadHandler && this.imageLoadHandler(image)
this.detectAnimation(image)
this.detectPixelArt(image)
this.drawThumbnail()
},
onError () {
this.imageLoadError && this.imageLoadError()
},
detectPixelArt (image) {
// Safe maximum: 32x32 image, equivalent or smaller
this.isPixelArt ||= image.naturalHeight * image.naturalWidth <= 32 * 32;
// Common size for oldweb badges.
this.isPixelArt ||= image.naturalWidth == 88 && image.naturalHeight == 31;
},
detectAnimation (image) {
const mediaProxyAvailable = this.$store.state.instance.mediaProxyAvailable
@ -73,51 +59,39 @@ const StillImage = {
},
detectAnimationWithFetch (image) {
// Browser Cache should ensure image doesn't get loaded twice if cache exists
return fetch(image.src, {
fetch(image.src, {
referrerPolicy: 'same-origin'
})
.then(data => {
// We don't need to read the whole file so only call it once
return data.body.getReader().read()
data.body.getReader().read()
.then(reader => {
// Ordered from least to most intensive
if (this.isGIF(reader.value)) {
this.isAnimated = true
this.setLabel('GIF')
return true
return
}
if (this.isAnimatedWEBP(reader.value)) {
this.isAnimated = true
this.setLabel('WEBP')
return true
return
}
if (this.isAnimatedPNG(reader.value)) {
this.isAnimated = true
this.setLabel('APNG')
return true
}
return false
})
})
.catch(() => {
// this.imageLoadError && this.imageLoadError()
return null
})
},
detectWithMediaProxy (image) {
this.detectAnimationWithFetch(image)
},
async detectWithoutMediaProxy (image) {
// If media is local, we can still fetch,
// otherwise CORS wont allow it and we fall back to checking extensions
// (XXX: ideally wed _only_ fetch if its an allowed domain;
// i.e. local media or proxy, but we currently have no way of knowing)
const classifiedAsAnim = await this.detectAnimationWithFetch(image)
if (classifiedAsAnim != null) {
return
}
// Otherwise we'll just make a guess based on extension
detectWithoutMediaProxy (image) {
// We'll just assume that gifs and webp are animated
const extension = image.src.split('.').pop().toLowerCase()
if (extension === 'gif') {
@ -130,15 +104,18 @@ const StillImage = {
this.setLabel('WEBP')
return
}
// Beware: APNGs also sometimes use just a plain png extension!
// (but this would mislabel too many images as "animated")
if (extension === 'apng') {
this.isAnimated = true
this.setLabel('APNG')
return
// Beware the apng! use this if ye dare
// if (extension === 'png') {
// this.isAnimated = true
// this.setLabel('PNG')
// return
// }
// Hail mary for extensionless
if (extension.includes('/')) {
// Don't mind the CORS error barrage
this.detectAnimationWithFetch(image)
}
// Hail mary for extensionless files we cannot fetch
},
setLabel (name) {
this.imageTypeLabel = name;
@ -230,7 +207,7 @@ const StillImage = {
}
context.clearRect(0, 0, canvas.width, canvas.height); // Clear the previous unscaled image
context.imageSmoothingEnabled = !this.isPixelArt;
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = 'high';
// Draw the good one for realsies

View file

@ -2,7 +2,7 @@
<div
ref="still-image"
class="still-image"
:class="{ animated: animated, pixelart: isPixelArt }"
:class="{ animated: animated }"
:style="style"
>
<div
@ -95,8 +95,5 @@
visibility: visible;
}
}
&.pixelart {
image-rendering: pixelated;
}
}
</style>

View file

@ -22,7 +22,6 @@ const Timeline = {
'title',
'userId',
'listId',
'conversationId',
'tag',
'embedded',
'count',
@ -120,7 +119,6 @@ const Timeline = {
showImmediately,
userId: this.userId,
listId: this.listId,
conversationId: this.conversationId,
tag: this.tag
})
},
@ -183,7 +181,6 @@ const Timeline = {
showImmediately: true,
userId: this.userId,
listId: this.listId,
conversationId: this.conversationId,
tag: this.tag
}).then(({ statuses }) => {
if (statuses && statuses.length === 0) {

View file

@ -14,13 +14,6 @@
z-index: 2;
}
.timeline-extra-heading {
width: 100%;
padding-top: 0.5em;
padding-bottom: 0.5em;
background-color: var(--panel, #182230);
}
&.-nonpanel {
.timeline-heading {
text-align: center;

View file

@ -52,12 +52,6 @@
</button>
</div>
</div>
<div
v-if="$slots.extraHeading"
class="timeline-extra-heading"
>
<slot name="extraHeading" />
</div>
<div :class="classes.body">
<div
ref="timeline"

View file

@ -13,7 +13,7 @@ export const timelineNames = () => {
return {
'friends': 'nav.home_timeline',
'bookmarks': 'nav.bookmarks',
'dm_conversation': 'nav.dm_conversation',
'dms': 'nav.dms',
'public-timeline': 'nav.public_tl',
'public-external-timeline': 'nav.twkn',
'bubble-timeline': 'nav.bubble_timeline'

View file

@ -80,6 +80,22 @@
>{{ $t("nav.bookmarks") }}</span>
</router-link>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'dms', params: { username: currentUser.screen_name } }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="envelope"
/>
<span
:title="$t('nav.dms')"
:aria-label="$t('nav.dms')"
>{{ $t("nav.dms") }}</span>
</router-link>
</li>
</ul>
</template>

View file

@ -80,6 +80,22 @@
>{{ $t("nav.bookmarks") }}</span>
</router-link>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'dms', params: { username: currentUser.screen_name } }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="envelope"
/>
<span
:title="$t('nav.dms')"
:aria-label="$t('nav.dms')"
>{{ $t("nav.dms") }}</span>
</router-link>
</li>
</ul>
</template>

View file

@ -19,6 +19,7 @@ export const timelineNames = () => {
return {
'friends': 'nav.home_timeline',
'bookmarks': 'nav.bookmarks',
'dms': 'nav.dms',
'public-timeline': 'nav.public_tl',
'public-external-timeline': 'nav.twkn',
'bubble-timeline': 'nav.bubble_timeline'

View file

@ -117,11 +117,6 @@ export default {
shouldConfirmMute () {
return this.mergedConfig.modalOnMute
},
compactUserInfo () {
return this.$store.getters.mergedConfig.compactUserInfo
&& (this.$store.state.interface.layoutType !== 'mobile')
&& this.switcher
},
...mapGetters(['mergedConfig'])
},
components: {
@ -197,7 +192,7 @@ export default {
this.$store.dispatch('setCurrentMedia', attachment)
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { repliedUser: this.user })
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
}
}
}

View file

@ -21,13 +21,6 @@
position: relative;
}
.user-buttons {
grid-area: edit;
display: flex;
padding: .5em 0 .5em 0;
justify-self: end;
}
.panel-body {
word-wrap: break-word;
border-bottom-right-radius: inherit;
@ -60,6 +53,7 @@
}
&-bio {
text-align: center;
display: block;
line-height: 1.3;
padding: 1em;
@ -106,14 +100,15 @@
padding: 0 26px;
.container {
min-width: 0;
padding: 16px 0 6px;
display: grid;
grid-template-areas:
"pfp name edit"
"pfp summary summary"
"stats stats stats";
grid-template-columns: auto 1fr auto;
align-items: start;
display: flex;
align-items: flex-start;
max-height: 56px;
> * {
min-width: 0;
}
.Avatar {
--_avatarShadowBox: var(--avatarShadow);
@ -128,7 +123,6 @@
}
&-avatar-link {
grid-area: pfp;
position: relative;
cursor: pointer;
@ -159,8 +153,8 @@
.external-link-button, .edit-profile-button {
cursor: pointer;
width: 2.3em;
text-align: right;
width: 2.5em;
text-align: center;
margin: -0.5em 0;
padding: 0.5em 0;
@ -171,16 +165,12 @@
}
.user-summary {
grid-area: summary;
display: grid;
grid-template-areas:
"name name name name name"
"hand role lock avg _";
grid-template-columns:
auto auto auto auto 1fr;
justify-items: start;
display: block;
margin-left: 0.6em;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 0;
// This is so that text doesn't get overlapped by avatar's shadow if it has
// big one
z-index: 1;
@ -188,80 +178,54 @@
--emoji-size: 1.7em;
.user-locked {
margin-left: 0.5em;
grid-area: lock;
}
.user-screen-name {
min-width: 1px;
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
grid-area: hand;
}
.dailyAvg {
min-width: 1px;
margin-left: 1em;
font-size: 0.7em;
color: $fallback--text;
color: var(--text, $fallback--text);
grid-area: avg;
}
.user-roles {
.top-line,
.bottom-line {
display: flex;
grid-area: role;
.user-role {
color: $fallback--text;
color: var(--alertNeutralText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--alertNeutral, $fallback--fg);
}
}
}
.user-counts {
grid-area: stats;
display: flex;
line-height:16px;
padding-top: 0.5em;
text-align: center;
justify-content: space-around;
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
align-self: center;
.user-count {
padding: .5em 0 .5em 0;
margin: 0 .5em;
h5 {
font-size:1em;
font-weight: bolder;
margin: 0 0 0.25em;
}
a {
text-decoration: none;
}
}
}
.user-name {
text-align: start;
text-overflow: ellipsis;
overflow: hidden;
margin-left: 0.6em;
flex: 1 1 auto;
margin-right: 1em;
font-size: 1.1em;
grid-area: name;
align-self: center;
white-space: nowrap;
max-width: 100%;
z-index: 1; // so shadow from user avatar doesn't overlap it
}
.bottom-line {
font-weight: light;
font-size: 1.1em;
align-items: baseline;
.lock-icon {
margin-left: 0.5em;
}
.user-screen-name {
min-width: 1px;
flex: 0 1 auto;
text-overflow: ellipsis;
overflow: hidden;
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
}
.dailyAvg {
min-width: 1px;
flex: 0 0 auto;
margin-left: 1em;
font-size: 0.7em;
color: $fallback--text;
color: var(--text, $fallback--text);
}
.user-role {
flex: none;
color: $fallback--text;
color: var(--alertNeutralText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--alertNeutral, $fallback--fg);
}
}
.user-meta {
@ -326,21 +290,34 @@
margin: 0;
}
}
&.-compact {
.container {
grid-template-areas:
"pfp name stats edit"
"pfp summary stats edit";
grid-template-columns: auto auto 1fr auto;
}
.user-counts {
padding-top: 0;
justify-content: space-evenly;
}
}
}
.sidebar .edit-profile-button {
display: none;
}
.user-counts {
display: flex;
line-height:16px;
padding: .5em 1.5em 0em 1.5em;
text-align: center;
justify-content: space-between;
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
flex-wrap: wrap;
}
.user-count {
flex: 1 0 auto;
padding: .5em 0 .5em 0;
margin: 0 .5em;
h5 {
font-size:1em;
font-weight: bolder;
margin: 0 0 0.25em;
}
a {
text-decoration: none;
}
}

View file

@ -9,10 +9,7 @@
class="background-image"
/>
<div class="panel-heading -flexible-height">
<div
class="user-info"
:class="{ '-compact': compactUserInfo }"
>
<div class="user-info">
<div class="container">
<a
v-if="allowZoomingAvatar"
@ -32,7 +29,6 @@
</a>
<router-link
v-else
class="user-info-avatar-link"
:to="userProfileLink(user)"
>
<UserAvatar
@ -40,124 +36,94 @@
:user="user"
/>
</router-link>
<RichContent
:title="user.name"
class="user-name"
:html="user.name"
:emoji="user.emoji"
/>
<div class="user-summary">
<router-link
class="user-screen-name"
:title="user.screen_name_ui"
:to="userProfileLink(user)"
>
@{{ user.screen_name_ui }}
</router-link>
<span
v-if="!hideBio && (user.deactivated || !!visibleRole || user.bot)"
class="user-roles"
>
<span
v-if="user.deactivated"
class="alert user-role"
<div class="top-line">
<RichContent
:title="user.name"
class="user-name"
:html="user.name"
:emoji="user.emoji"
/>
<button
v-if="!isOtherUser && user.is_local"
class="button-unstyled edit-profile-button"
@click.stop="openProfileTab"
>
{{ $t('user_card.deactivated') }}
<FAIcon
fixed-width
class="icon"
icon="edit"
:title="$t('user_card.edit_profile')"
/>
</button>
<a
v-if="isOtherUser && !user.is_local"
:href="user.statusnet_profile_url"
target="_blank"
class="button-unstyled external-link-button"
>
<FAIcon
class="icon"
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"
:relationship="relationship"
/>
</div>
<div class="bottom-line">
<router-link
class="user-screen-name"
:title="user.screen_name_ui"
:to="userProfileLink(user)"
>
@{{ user.screen_name_ui }}
</router-link>
<template v-if="!hideBio">
<span
v-if="user.deactivated"
class="alert user-role"
>
{{ $t('user_card.deactivated') }}
</span>
<span
v-if="!!visibleRole"
class="alert user-role"
>
{{ $t(`general.role.${visibleRole}`) }}
</span>
<span
v-if="user.bot"
class="alert user-role"
>
{{ $t('user_card.bot') }}
</span>
</template>
<span v-if="user.locked">
<FAIcon
class="lock-icon"
icon="lock"
size="sm"
/>
</span>
<span
v-if="!!visibleRole"
class="alert user-role"
>
{{ $t(`general.role.${visibleRole}`) }}
</span>
<span
v-if="user.bot"
class="alert user-role"
>
{{ $t('user_card.bot') }}
</span>
</span>
<span
v-if="user.locked"
class="user-locked"
>
<FAIcon
class="lock-icon"
icon="lock"
size="sm"
/>
</span>
<span
v-if="!mergedConfig.hideUserStats && !hideBio"
class="dailyAvg"
>{{ dailyAvg }} {{ $t('user_card.per_day') }}</span>
</div>
<div
v-if="!mergedConfig.hideUserStats && switcher"
class="user-counts"
>
<div
class="user-count"
@click.prevent="setProfileView('statuses')"
>
<h5>{{ $t('user_card.statuses') }}</h5>
<span>{{ user.statuses_count }} <br></span>
v-if="!mergedConfig.hideUserStats && !hideBio"
class="dailyAvg"
>{{ dailyAvg }} {{ $t('user_card.per_day') }}</span>
</div>
<div
class="user-count"
@click.prevent="setProfileView('friends')"
>
<h5>{{ $t('user_card.followees') }}</h5>
<span>{{ hideFollowsCount ? $t('user_card.hidden') : user.friends_count }}</span>
</div>
<div
class="user-count"
@click.prevent="setProfileView('followers')"
>
<h5>{{ $t('user_card.followers') }}</h5>
<span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span>
</div>
</div>
<div class="user-buttons">
<button
v-if="!isOtherUser && user.is_local"
class="button-unstyled edit-profile-button"
@click.stop="openProfileTab"
>
<FAIcon
fixed-width
class="icon"
icon="edit"
:title="$t('user_card.edit_profile')"
/>
</button>
<a
v-if="isOtherUser && !user.is_local"
:href="user.statusnet_profile_url"
target="_blank"
class="button-unstyled external-link-button"
>
<FAIcon
class="icon"
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"
:relationship="relationship"
/>
</div>
</div>
<div class="user-meta">
@ -303,13 +269,38 @@
v-if="!hideBio"
class="panel-body"
>
<div
v-if="!mergedConfig.hideUserStats && switcher"
class="user-counts"
>
<div
class="user-count"
@click.prevent="setProfileView('statuses')"
>
<h5>{{ $t('user_card.statuses') }}</h5>
<span>{{ user.statuses_count }} <br></span>
</div>
<div
class="user-count"
@click.prevent="setProfileView('friends')"
>
<h5>{{ $t('user_card.followees') }}</h5>
<span>{{ hideFollowsCount ? $t('user_card.hidden') : user.friends_count }}</span>
</div>
<div
class="user-count"
@click.prevent="setProfileView('followers')"
>
<h5>{{ $t('user_card.followers') }}</h5>
<span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span>
</div>
</div>
<RichContent
v-if="!hideBio"
class="user-card-bio"
:html="user.description_html"
:emoji="user.emoji"
:handle-links="true"
:style='{"text-align": $store.getters.mergedConfig.centerAlignBio ? "center" : "start"}'
/>
</div>
<teleport to="#modal">

View file

@ -486,7 +486,6 @@
"cGreen": "Grün (Retweet)",
"cOrange": "Orange (Favorisieren)",
"cRed": "Rot (Abbrechen)",
"center_align_bio": "Zentrale Textausrichtung in der Bio",
"change_email": "Ändere Email",
"change_email_error": "Es trat ein Problem auf beim Versuch, deine Email Adresse zu ändern.",
"change_password": "Passwort ändern",
@ -497,7 +496,6 @@
"checkboxRadius": "Auswahlfelder",
"collapse_subject": "Beiträge mit Inhaltswarnungen einklappen",
"columns": "Spalten",
"compact_user_info": "Kompakte Benutzerinfos wenn genug Platz",
"composing": "Verfassen",
"confirm_dialogs": "Bestätigung erforderlich für:",
"confirm_dialogs_approve_follow": "Annehmen einer Followanfrage",
@ -936,7 +934,6 @@
"title": "Version"
},
"virtual_scrolling": "Anzeige der Zeitleiste optimieren",
"widen_timeline": "Zeitleiste verbreitern, um horizontalen Platz zu füllen",
"word_filter": "Wortfilter",
"wordfilter": "Wortfilter"
},

View file

@ -7,21 +7,15 @@
"replace": "Αντικατάσταση"
},
"mrf_policies": "Ενεργοποιημένες πολιτικές MRF",
"mrf_policies_desc": "Οι πολιτικές MRF επηρεάζουν τη συμπεριφορά του instance. Οι ακόλουθες πολιτικές είναι ενεργοποιημένες:",
"mrf_policies_desc": "",
"simple": {
"accept": "Αποδοχή",
"accept_desc": "Αυτό το instance αποδέχεται μηνύματα μόνο από τα ακόλουθα instances:",
"ftl_removal": "Αφαίρεση από το χρονολόγιο \"Γνωστού Δίκτυου\"",
"ftl_removal_desc": "Αυτό το instance αφαιρεί αυτά τα instances από το χρονολόγιο \"Γνωστού Δικτύου\":",
"media_nsfw": "Επιβολή ορισμού μέσων ως ευαίσθητων",
"media_nsfw_desc": "Αυτό το instance αναγκάζει τα μέσα να ορίζονται ως ευαίσθητα στις αναρτήσεις στα ακόλουθα instances:",
"media_removal": "Αφαίρεση Μέσων",
"media_removal_desc": "Αυτό το instance αφαιρεί τα μέσα από τις αναρτήσεις των ακόλουθων instances:",
"quarantine": "Καραντίνα",
"quarantine_desc": "Αυτό το instance δε θα στέλνει αναρτήσεις στα ακόλουθα instances:",
"reason": "Λόγος",
"reject": "Απόρριψη",
"reject_desc": "Αυτό το instance δε θα δέχεται μηνύματα από τα παρακάτω instances:",
"simple_policies": "Πολιτικές του instance"
}
},
@ -38,20 +32,11 @@
"inactive_message": "Αυτή η ανακοίνωση είναι ανενεργή",
"page_header": "Ανακοινώσεις",
"post_action": "Ανάρτηση",
"post_error": "Σφάλμα: {error}",
"post_form_header": "Ανάρτηση ανακοίνωσης",
"post_placeholder": "Περιεχόμενο ανακοίνωσης",
"published_time_display": "Δημοσιεύτηκε {time}",
"start_time_display": "Ξεκινάει από {time}",
"title": "Ανακοίνωση"
},
"chats": {
"chats": "Συνομιλίες",
"delete": "Διαγραφή",
"delete_confirm": "Θέλετε σίγουρα να διαγράψετε αυτό το μήνυμα;",
"empty_chat_list_placeholder": "Δεν έχετε καμία συνομιλία. Ξεκινήστε μια νέα συνομιλία!",
"empty_message_error": "Δε μπορεί να σταλεί κενό μήνυμα",
"error_loading_chat": "Κάτι δεν πήγε καλά κατά τη φόρτωση της συνομιλίας.",
"error_sending_message": "Κάτι πήγε λάθος κατά την αποστολή του μηνύματος.",
"message_user": "Στείλε μήνυμα στον/στην {nickname}",
"more": "Περισσότερα",
@ -62,15 +47,11 @@
"today": "Σήμερα"
},
"domain_mute_card": {
"mute": "Σίγαση",
"mute_progress": "Σίγαση…",
"unmute": "Αφαίρεση σίγασης",
"unmute_progress": "Αφαίρεση σίγασης…"
"mute": "Σίγαση"
},
"emoji": {
"add_emoji": "Εισαγωγή emoji",
"load_all": "Φόρτωση όλων των {emojiAmount} emoji",
"load_all_hint": "Φορτώθηκαν τα πρώτα {saneAmount} emoji, η φόρτωση όλων των emoji μπορεί να προκαλέσει θέματα απόδοσης.",
"recent": "Χρησιμοποιήθηκαν πρόσφατα",
"search_emoji": "Αναζήτηση για ένα emoji",
"stickers": "Αυτοκόλλητα"
@ -82,10 +63,7 @@
"export": "Εξαγωγή"
},
"features_panel": {
"media_proxy": "Διαμεσολαβητής μέσων",
"text_limit": "Όριο κειμένου",
"title": "Δυνατότητες",
"upload_limit": "Όριο upload"
"text_limit": "Όριο κειμένου"
},
"file_type": {
"audio": "Ήχος",
@ -101,8 +79,6 @@
"enable": "Ενεργοποίηση",
"error_retry": "Παρακαλώ δοκιμάστε ξανά",
"flash_content": "Κάντε κλικ για την εμφάνιση Flash περιεχομένου με τη χρήση του Ruffle (Πειραματικό, μπορεί να μη λειτουργεί).",
"flash_fail": "Η φόρτωση περιεχομένου flash απέτυχε, δείτε στην κονσόλα για λεπτομέρειες.",
"generic_error": "Προέκυψε ένα σφάλμα",
"loading": "Φόρτωση…",
"more": "Περισσότερα",
"optional": "προαιρετικό",
@ -128,7 +104,6 @@
"save_without_cropping": "Αποθήκευση χωρίς περικοπή"
},
"importer": {
"error": "Προέκυψε ένα σφάλμα κατά την εισαγωγή αυτού του αρχείου.",
"success": "Εισήχθη επιτυχώς."
},
"languages": {

View file

@ -243,27 +243,6 @@
"search": "Search users",
"title": "List title"
},
"dm_conv": {
"default_name": "Conversation {id}",
"delete_confirm": "Are you sure you want to remove the conversation {identifier}?",
"delete_confirm_accept_button": "Yes, remove",
"delete_confirm_cancel_button": "No, keep",
"delete_confirm_title": "Remove DM conversation",
"delete_tooltip": "remove conversation from list",
"last_active_date": "Last activity",
"last_active_member": "Last active",
"last_message_title": "Last message",
"mark_all_read_button": "Mark all as read",
"mark_single_read_tooltip": "mark as read",
"page_header": "Direct Conversation",
"recipients_edit_add_new_title": "Add new member:",
"recipients_edit_current_title": "Core members besides yourself:",
"recipients_edit_mode_button": "Edit core members",
"recipients_edit_mode_button_tooltip": "Edit core members",
"recipients_edit_title": "Core members of {conversation_name}",
"recipients_save": "Save changes to core members",
"unread_msg": "has unread messages"
},
"login": {
"authentication_code": "Authentication code",
"description": "Log in with OAuth",
@ -327,9 +306,6 @@
"bubble_timeline": "Bubble timeline",
"bubble_timeline_description": "Posts from instances close to yours, as recommended by the admins",
"chats": "Chats",
"dm_conv_list": "Direct Conversations",
"dm_conversation": "Direct Conversation",
"dm_conversations": "Direct Conversations",
"dms": "Direct messages",
"friend_requests": "Follow requests",
"home_timeline": "Home timeline",
@ -512,7 +488,6 @@
"blocks_tab": "Blocks",
"bot": "This is a bot account",
"btnRadius": "Buttons",
"center_align_bio": "Center text in user bio",
"cBlue": "Blue (Reply, follow)",
"cGreen": "Green (Retweet)",
"cOrange": "Orange (Favorite)",
@ -527,13 +502,11 @@
"checkboxRadius": "Checkboxes",
"collapse_subject": "Collapse posts with content warnings",
"columns": "Columns",
"compact_user_info": "Compact user info when enough space",
"composing": "Composing",
"confirm_dialogs": "Require confirmation for:",
"confirm_dialogs_approve_follow": "Accepting a follow request",
"confirm_dialogs_block": "Blocking someone",
"confirm_dialogs_delete": "Deleting a post",
"confirm_dialogs_delete_dm_conv": "Deleting a DM conversation",
"confirm_dialogs_deny_follow": "Rejecting a follow request",
"confirm_dialogs_mute": "Muting someone",
"confirm_dialogs_repeat": "Repeating a post",
@ -753,7 +726,7 @@
"security": "Security",
"security_tab": "Security",
"sensitive_by_default": "Mark posts as sensitive by default",
"sensitive_if_subject": "Automatically mark post as sensitive if a content warning is specified",
"sensitive_if_subject": "Automatically mark images as sensitive if a content warning is specified",
"set_new_avatar": "Set new avatar",
"set_new_mascot": "Set new mascot",
"set_new_profile_background": "Set new profile background",
@ -975,7 +948,6 @@
},
"virtual_scrolling": "Optimize timeline rendering",
"use_blurhash": "Use blurhashes for NSFW thumbnails",
"widen_timeline": "Widen the Timeline to fill horizontal space",
"word_filter": "Word filter",
"wordfilter": "Wordfilter"
},

View file

@ -407,8 +407,7 @@
"private": "Ce statut sera visible par seulement vos abonné⋅e⋅s",
"public": "Ce statut sera visible par tout le monde",
"unlisted": "Ce statut ne sera pas visible dans les flux publics ni les flux fédérés"
},
"toggle_content_warning": "Activer/désactiver l'avertissement"
}
},
"registration": {
"awaiting_email_confirmation": "Votre compte a été enregistré et un courriel envoyé à votre adresse. Veuillez consulter votre boîte mail pour terminer la registration.",
@ -492,7 +491,6 @@
"cGreen": "Vert (partager)",
"cOrange": "Orange (aimer)",
"cRed": "Rouge (annuler)",
"center_align_bio": "Centrer le texte de la biographie",
"change_email": "Changer de courriel",
"change_email_error": "Il y a eu un problème pour changer votre courriel.",
"change_password": "Changez votre mot de passe",
@ -503,7 +501,6 @@
"checkboxRadius": "Cases à cocher",
"collapse_subject": "Réduire les messages avec des avertissements",
"columns": "Colonnes",
"compact_user_info": "Utiliser l'affichage compacte des biographies quand possible",
"composing": "Composition",
"confirm_dialogs": "Demander confirmation :",
"confirm_dialogs_approve_follow": "Accepter une demande de suivi",
@ -539,8 +536,6 @@
"enable_web_push_notifications": "Activer les notifications de push web",
"enter_current_password_to_confirm": "Entrez votre mot de passe actuel pour confirmer votre identité",
"expert_mode": "Avancé",
"expire_posts_enabled": "Supprimer les statuts après le nombre de jours demandé",
"expire_posts_input_placeholder": "Nombre de jours",
"export_theme": "Enregistrer le thème",
"file_export_import": {
"backup_restore": "Sauvegarde des Paramètres",
@ -604,7 +599,7 @@
"list_backups_error": "Erreur pendant la sauvegarde des listes: {error}",
"lock_account_description": "Limitez votre compte aux abonnés acceptés uniquement",
"loop_video": "Vidéos en boucle",
"loop_video_silent_only": "Boucle uniquement les vidéos sans son (les « gifs » de Mastodon)",
"loop_video_silent_only": "Boucle uniquement les vidéos sans le son (les « gifs » de Mastodon)",
"mascot": "Mascotte de l'interface Mastodon",
"max_depth_in_thread": "Nombre maximum de niveaux à afficher dans les fils par défaut",
"max_thumbnails": "Nombre maximum de miniatures par statuts",
@ -682,9 +677,7 @@
"pad_emoji": "Entourer les émoji d'espaces après leur sélections",
"panelRadius": "Fenêtres",
"pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas actif",
"permit_followback_description": "Accepter toutes les demandes de suivi envoyées par les comptes que vous suivez",
"play_videos_in_modal": "Jouer les vidéos directement dans le visionneur de médias",
"post_language": "Langue par défaut des statuts",
"post_look_feel": "Apparence des status",
"post_status_content_type": "Type de contenu du statuts",
"posts": "Statuts",
@ -752,7 +745,6 @@
"show_admin_badge": "Afficher le badge d'Admin sur mon profil",
"show_moderator_badge": "Afficher le badge de Modo' sur mon profil",
"show_nav_shortcuts": "Afficher plus de raccourcis de navigations dans le panneau supérieur",
"show_page_backgrounds": "Afficher des fonds d'écran propres aux pages, ex : les biographies",
"show_panel_nav_shortcuts": "Afficher les raccourcis de navigation du flux dans le panneau supérieur",
"show_scrollbars": "Afficher la barre de défilement",
"show_wider_shortcuts": "Plus d'espace entre les raccourcis dans le panneau supérieur",
@ -928,13 +920,8 @@
"upload_a_photo": "Envoyer une photo",
"useStreamingApi": "Recevoir les messages et notifications en temps réel",
"useStreamingApiWarning": "(Non recommandé, expérimental, connu pour rater des messages)",
"use_blurhash": "Flouter les miniatures des images sensibles",
"use_contain_fit": "Ne pas rogner les miniatures des pièces-jointes",
"use_one_click_nsfw": "Ouvrir les pièces-jointes sensibles avec un seul clic",
"user_accepts_direct_messages_from": "Accepter les messages directs de",
"user_accepts_direct_messages_from_everybody": "Tout le monde",
"user_accepts_direct_messages_from_nobody": "Personne",
"user_accepts_direct_messages_from_people_i_follow": "Comptes que je suis",
"user_mutes": "Comptes",
"user_profile_default_tab": "Onglet affiché par défaut dans les profils",
"user_profiles": "Profils utilisateurs",
@ -1057,7 +1044,6 @@
"collapse": "Fermer",
"conversation": "Conversation",
"error": "Erreur lors de l'affichage du flux : {0}",
"follow_tag": "Suivre le hashtag",
"load_older": "Afficher des status plus ancien",
"no_more_statuses": "Pas plus de statuts",
"no_retweet_hint": "Le message est marqué en abonnés-seulement ou direct et ne peut pas être partagé",
@ -1067,7 +1053,6 @@
"show_new": "Afficher plus",
"socket_broke": "Connexion temps-réel perdue : CloseEvent code {0}",
"socket_reconnected": "Connexion temps-réel établie",
"unfollow_tag": "Désabonner du hashtag",
"up_to_date": "À jour"
},
"toast": {
@ -1132,7 +1117,6 @@
"block_confirm_title": "Bloquer l'utilisateur",
"block_progress": "Blocage…",
"blocked": "Bloqué !",
"blocks_you": "Vous bloque !",
"bot": "Robot",
"deactivated": "Désactivé",
"deny": "Rejeter",
@ -1147,10 +1131,7 @@
"follow_cancel": "Annuler la demande d'abonnement",
"follow_progress": "Demande en cours…",
"follow_sent": "Demande envoyée !",
"follow_tag": "Suivre le hashtag",
"follow_unfollow": "Désabonner",
"followed_tags": "Hashtags suivis",
"followed_users": "Utilisateurs suivis",
"followees": "Abonnements",
"followers": "Abonné·es",
"following": "Suivi !",
@ -1175,14 +1156,12 @@
"mute_domain": "Bloquer la domaine",
"mute_progress": "Masquage…",
"muted": "Masqué",
"not_following_any_hashtags": "Aucun hashtag suivi",
"note": "Note privée",
"per_day": "par jour",
"remote_follow": "Suivre d'une autre instance",
"remove_follower": "Désabonner",
"replies": "Statuts et réponses",
"report": "Signalement",
"requested_by": "Vous a envoyé une demande de suivi",
"show_repeats": "Montrer les partages",
"statuses": "Statuts",
"subscribe": "Abonner",
@ -1192,13 +1171,11 @@
"unfollow_confirm_accept_button": "Oui : me désabonner",
"unfollow_confirm_cancel_button": "Non : garder l'abonnement",
"unfollow_confirm_title": "Désabonner",
"unfollow_tag": "Désabonner du hashtag",
"unmute": "Démasquer",
"unmute_progress": "Démasquage…",
"unsubscribe": "Désabonner"
},
"user_profile": {
"field_validated": "Lien vérifié",
"profile_does_not_exist": "Désolé, ce profil n'existe pas.",
"profile_loading_error": "Désolé, il y a eu une erreur au chargement du profil.",
"timeline_title": "Flux du compte"

View file

@ -503,13 +503,7 @@
"columns": "Colonne",
"composing": "Composizione",
"confirm_new_password": "Conferma la nuova password",
"conversation_display": "Stile di visualizzazione delle conversazioni",
"conversation_display_linear": "Stile lineare",
"conversation_display_tree": "Stile ad albero",
"conversation_other_replies_button": "Mostra il bottone \"altre risposte\"",
"conversation_other_replies_button_below": "Sotto i post",
"current_avatar": "La tua icona attuale",
"current_mascot": "La tua mascotte attuale",
"current_password": "La tua password attuale",
"data_import_export_tab": "Importa o esporta dati",
"default_vis": "Visibilità predefinita dei messaggi",
@ -517,15 +511,11 @@
"delete_account_description": "Elimina definitivamente i tuoi dati e disattiva il tuo profilo.",
"delete_account_error": "C'è stato un problema durante l'eliminazione del tuo profilo. Se il problema persiste contatta l'amministratore della tua stanza.",
"delete_account_instructions": "Digita la tua password nel campo sottostante per eliminare il tuo profilo.",
"disable_sticky_headers": "Non fissare i titoli delle colonne in cima allo schermo",
"discoverable": "Permetti la scoperta di questo profilo a servizi di ricerca ed altro",
"domain_mutes": "Domini",
"download_backup": "Scarica",
"email_language": "Lingua delle email ricevute dal server",
"emoji_reactions_on_timeline": "Mostra reazioni nelle sequenze",
"enable_web_push_notifications": "Abilita notifiche web push",
"enter_current_password_to_confirm": "Inserisci la tua password per identificarti",
"expert_mode": "Mostra avanzate",
"export_theme": "Salva impostazioni",
"file_export_import": {
"backup_restore": "Archiviazione impostazioni",
@ -553,23 +543,18 @@
"hide_all_muted_posts": "Nascondi messaggi silenziati",
"hide_attachments_in_convo": "Nascondi gli allegati presenti nelle conversazioni",
"hide_attachments_in_tl": "Nascondi gli allegati presenti nelle sequenze",
"hide_bot_indication": "Nascondi indicatore bot nei post",
"hide_favorites_description": "Non mostrare la lista dei miei preferiti (gli utenti verranno comunque notificati)",
"hide_filtered_statuses": "Nascondi messaggi filtrati",
"hide_followers_count_description": "Non mostrare quanti seguaci ho",
"hide_followers_description": "Non mostrare i miei seguaci",
"hide_follows_count_description": "Non mostrare quanti utenti seguo",
"hide_follows_description": "Non mostrare chi seguo",
"hide_isp": "Nascondi pannello della stanza",
"hide_list_aliases_error_action": "Chiudi",
"hide_media_previews": "Nascondi anteprime",
"hide_muted_posts": "Nascondi messaggi degli utenti silenziati",
"hide_muted_threads": "Nascondi conversazioni silenziate",
"hide_post_stats": "Nascondi statistiche dei messaggi (es. il numero di preferenze)",
"hide_shoutbox": "Nascondi muro dei graffiti",
"hide_user_stats": "Nascondi statistiche dell'utente (es. il numero di seguaci)",
"hide_wallpaper": "Nascondi sfondo della stanza",
"hide_wordfiltered_statuses": "Nascondi post filtrati per parola",
"import_blocks_from_a_csv_file": "Importa blocchi da un file CSV",
"import_followers_from_a_csv_file": "Importa una lista di chi segui da un file CSV",
"import_mutes_from_a_csv_file": "Importa silenziati da un file CSV",
@ -582,14 +567,10 @@
"invalid_theme_imported": "Il file selezionato non è un tema supportato da Pleroma. Il tuo tema non è stato modificato.",
"limited_availability": "Non disponibile nel tuo browser",
"links": "Collegamenti",
"list_aliases_error": "Errore nel recupero degli alias: {error}",
"list_backups_error": "Errore nel recupero della lista dei backup: {error}",
"lock_account_description": "Vaglia manualmente i nuovi seguaci",
"loop_video": "Riproduci video in ciclo continuo",
"loop_video_silent_only": "Riproduci solo video muti in ciclo continuo (es. le \"gif\" di Mastodon)",
"mascot": "Mascotte di MastodonFE",
"max_thumbnails": "Numero massimo di anteprime per messaggio",
"mention_links": "Collegamenti delle menzioni",
"mfa": {
"authentication_methods": "Metodi di accesso",
"confirm_and_enable": "Conferma ed abilita OTP",
@ -613,12 +594,6 @@
},
"minimal_scopes_mode": "Riduci opzioni di visibilità",
"more_settings": "Altre impostazioni",
"move_account": "Sposta account",
"move_account_error": "Errore nello spostamento dell'account: {error}",
"move_account_notes": "Se vuoi spostare questo account da qualche altra parte, devi andare all'account di destinazione e aggiungere un alias che punta qui.",
"move_account_target": "Account di destinazione (es. {example})",
"moved_account": "Account spostato.",
"mute_bot_posts": "Silenzia post dei bot",
"mute_export": "Esporta silenziati",
"mute_export_button": "Esporta i silenziati in un file CSV",
"mute_import": "Carica silenziati",
@ -628,7 +603,6 @@
"mutes_tab": "Silenziati",
"name": "Nome",
"name_bio": "Nome ed introduzione",
"new_alias_target": "Aggiungi nuovo alias (es. {example})",
"new_email": "Nuova email",
"new_password": "Nuova password",
"no_blocks": "Nessun utente bloccato",
@ -646,7 +620,6 @@
"notification_visibility_likes": "Preferiti",
"notification_visibility_mentions": "Menzioni",
"notification_visibility_moves": "Migrazioni utenti",
"notification_visibility_polls": "Termine dei poll in cui hai votato",
"notification_visibility_repeats": "Condivisioni",
"notifications": "Notifiche",
"nsfw_clickthrough": "Fai click per visualizzare gli allegati offuscati",
@ -655,9 +628,7 @@
"panelRadius": "Pannelli",
"pause_on_unfocused": "Interrompi l'aggiornamento continuo mentre la scheda è in secondo piano",
"play_videos_in_modal": "Riproduci video in un riquadro a sbalzo",
"post_look_feel": "Aspetto dei post",
"post_status_content_type": "Tipo di contenuto dei messaggi",
"posts": "Post",
"preload_images": "Precarica immagini",
"presets": "Valori predefiniti",
"profile_background": "Sfondo del tuo profilo",
@ -671,8 +642,6 @@
"profile_tab": "Profilo",
"radii_help": "Imposta il raggio degli angoli (in pixel)",
"refresh_token": "Aggiorna token",
"remove_alias": "Rimuovi questo alias",
"remove_backup": "Elimina",
"replies_in_timeline": "Risposte nelle sequenze",
"reply_visibility_all": "Mostra tutte le risposte",
"reply_visibility_following": "Mostra solo le risposte rivolte a me o agli utenti che seguo",
@ -697,15 +666,12 @@
"security_tab": "Sicurezza",
"sensitive_by_default": "Tutti i miei messaggi sono scabrosi",
"set_new_avatar": "Scegli una nuova icona",
"set_new_mascot": "Imposta nuova mascotte",
"set_new_profile_background": "Scegli un nuovo sfondo",
"set_new_profile_banner": "Scegli un nuovo gonfalone",
"setting_changed": "Valore personalizzato",
"setting_server_side": "Questa impostazione è legata al tuo profilo e ha effetto su tutte le sessioni e tutti i client",
"settings": "Impostazioni",
"show_admin_badge": "Mostra l'insegna di amministratore sul mio profilo",
"show_moderator_badge": "Mostra l'insegna di moderatore sul mio profilo",
"show_scrollbars": "Mostra le barre di scorrimento delle colonne laterali",
"stop_gifs": "Riproduci GIF al passaggio del cursore",
"streaming": "Mostra automaticamente i nuovi messaggi quando sei in cima alla pagina",
"style": {
@ -814,80 +780,66 @@
},
"filter_hint": {
"always_drop_shadow": "Attenzione: quest'ombra usa sempre {0} se il tuo browser lo supporta.",
"avatar_inset": "Tieni presente che combinare ombre (sia incavate che non) sulle icone utente potrebbe dare risultati strani con avatar trasparenti.",
"drop_shadow_syntax": "{0} non supporta il parametro {1} con la keyword {2}.",
"inset_classic": "Le ombre incavate usano {0}",
"spread_zero": "Le ombre con espansione maggiore di zero appariranno come se l'espansione fosse zero"
"avatar_inset": "Tieni presente che combinare ombre (sia incluse che non) sulle icone utente potrebbe dare risultati strani con quelle trasparenti.",
"drop_shadow_syntax": "{0} non supporta il parametro {1} né la keyword {2}.",
"inset_classic": "Le ombre incluse usano {0}",
"spread_zero": "Lo spandimento maggiore di zero si azzera sulle ombre"
},
"hintV3": "Per le ombre puoi anche usare la sintassi {0} per usare l'altro slot colore.",
"inset": "Incavatura",
"override": "Sovrascrivi",
"shadow_id": "Ombra #{value}",
"spread": "Espansione"
"hintV3": "Per le ombre puoi anche usare la sintassi {0} per sfruttare il secondo colore.",
"inset": "Includi",
"override": "Sostituisci",
"shadow_id": "Ombra numero {value}",
"spread": "Spandi"
},
"switcher": {
"clear_all": "Azzera tutto",
"clear_opacity": "Azzera opacità",
"clear_opacity": "Rimuovi opacità",
"help": {
"fe_downgraded": "La versione di PleromaFE è riportata ad una versione precedente.",
"fe_upgraded": "Il motore dei temi di PleromaFE è stato aggiornato insieme all'interfaccia.",
"future_version_imported": "Il tema importato è stato creato per una versione più nuova del frontend.",
"migration_napshot_gone": "Per qualche motivo non è stata trovata l'anteprima del tema, non tutto potrebbe essere come ricordi.",
"migration_snapshot_ok": "Per sicurezza, è stata caricata l'anteprima del tema. Puoi provare a caricarne i contenuti.",
"older_version_imported": "Il file importato è stato creato per una versione precedente del frontend.",
"snapshot_missing": "Il file non è provvisto di anteprima, quindi potrebbe essere diverso da come appare.",
"fe_downgraded": "L'interfaccia è stata portata ad una versione precedente.",
"fe_upgraded": "Lo schema dei temi è stato aggiornato insieme all'interfaccia.",
"future_version_imported": "Il tema importato è stato creato per una versione più recente dell'interfaccia.",
"migration_napshot_gone": "Anteprima del tema non trovata, non tutto potrebbe essere come ricordi.",
"migration_snapshot_ok": "Ho caricato l'anteprima del tema. Puoi provare a caricarne i contenuti.",
"older_version_imported": "Il tema importato è stato creato per una versione precedente dell'interfaccia.",
"snapshot_missing": "Il tema non è provvisto di anteprima, quindi potrebbe essere diverso da come appare.",
"snapshot_present": "Tutti i valori sono sostituiti dall'anteprima del tema. Puoi invece caricare i suoi contenuti.",
"snapshot_source_mismatch": "Conflitto di versione: probabilmente il frontend è stato deaggiornato e poi aggiornato di nuovo. Se hai modificato il tema con una vecchia versione usa il tema precedente, altrimenti usa quello nuovo.",
"upgraded_from_v2": "PleromaFE è stato aggiornato, il tema potrebbe essere un pochino diverso da come lo ricordi.",
"v2_imported": "Il file importato è stato creato per un vecchio frontend. Cerchiamo di massimizzare la compatibilità, ma potrebbero esserci inconsistenze."
"snapshot_source_mismatch": "Conflitto di versione: probabilmente l'interfaccia è stata portata indietro e poi aggiornata di nuovo. Se hai modificato il tema con una vecchia versione usa il tema precedente, altrimenti puoi usare il nuovo.",
"upgraded_from_v2": "L'interfaccia è stata aggiornata, il tema potrebbe essere diverso da come lo ricordi.",
"v2_imported": "Il tema importato è stato creato per una vecchia interfaccia. Non tutto potrebbe essere come inteso."
},
"keep_as_is": "Mantieni com'è",
"keep_as_is": "Mantieni tal quale",
"keep_color": "Mantieni colori",
"keep_fonts": "Mantieni font",
"keep_opacity": "Mantieni opacità",
"keep_roundness": "Mantieni vertici",
"keep_shadows": "Mantieni ombre",
"load_theme": "Carica tema",
"reset": "Azzera",
"reset": "Reimposta",
"save_load_hint": "Le opzioni \"mantieni\" conservano le impostazioni correnti quando selezioni o carichi un tema, e le salvano quando ne esporti uno. Quando nessuna casella è selezionata, tutte le impostazioni correnti saranno salvate nel tema.",
"use_snapshot": "Versione precedente",
"use_source": "Nuova versione"
}
},
"subject_input_always_show": "Mostra sempre il campo avvertenza sul contenuto",
"subject_line_behavior": "Copia avvertenza sul contenuto quando rispondi",
"subject_line_email": "Come nelle email: \"re: avvertenza\"",
"subject_line_mastodon": "Come su Mastodon: copia com'è",
"subject_input_always_show": "Mostra sempre il campo Oggetto",
"subject_line_behavior": "Copia oggetto quando rispondi",
"subject_line_email": "Come nelle email: \"re: oggetto\"",
"subject_line_mastodon": "Come in Mastodon: copia tal quale",
"subject_line_noop": "Non copiare",
"text": "Testo",
"theme": "Tema",
"theme_help": "Usa colori esadecimali (#rrvvbb) per personalizzare il tuo tema colori.",
"theme_help_v2_1": "Puoi anche sovrascrivere colore ed opacità di alcuni elementi spuntando la casella. Usa il pulsante \"Azzera\" per azzerare tutte le sovrascritture.",
"theme_help_v2_2": "Le icone vicino alcuni elementi sono indicatori del contrasto fra testo e sfondo, passaci sopra col puntatore per ulteriori informazioni. Se usano la trasparenza, questi indicatori mostrano come sarebbero nel peggior caso possibile.",
"third_column_mode": "Quando c'è abbastanza spazio, mostra una terza colonna contenente",
"third_column_mode_none": "Non mostrare proprio la terza colonna",
"third_column_mode_notifications": "Colonna notifiche",
"third_column_mode_postform": "Modulo post principale e navigazione",
"theme_help": "Usa colori esadecimali (#rrggbb) per personalizzare il tuo schema di colori.",
"theme_help_v2_1": "Puoi anche forzare colore ed opacità di alcuni elementi selezionando la casella. Usa il pulsante \"Azzera\" per azzerare tutte le forzature.",
"theme_help_v2_2": "Le icone vicino alcuni elementi sono indicatori del contrasto fra testo e sfondo, passaci sopra col puntatore per ulteriori informazioni. Se usani trasparenze, questi indicatori mostrano il peggior caso possibile.",
"token": "Token",
"tooltipRadius": "Suggerimenti/allerte",
"translation_language": "Lingua finale di traduzione automatica",
"tree_advanced": "Mostra bottoni aggiuntivi per aprire e chiudere catene di risposte nelle conversazioni",
"tree_fade_ancestors": "Mostra antenati del post corrente in testo semitrasparente",
"tooltipRadius": "Suggerimenti/avvisi",
"type_domains_to_mute": "Cerca domini da silenziare",
"upload_a_photo": "Carica una foto",
"upload_a_photo": "Carica un'immagine",
"useStreamingApi": "Ricevi messaggi e notifiche in tempo reale",
"useStreamingApiWarning": "",
"use_blurhash": "Usa blurhash per anteprime NSFW",
"useStreamingApiWarning": "(Sconsigliato, sperimentale, può saltare messaggi)",
"use_contain_fit": "Non ritagliare le anteprime degli allegati",
"use_one_click_nsfw": "Apri allegati NSFW con un solo click",
"user_accepts_direct_messages_from": "Accetta post «diretti» da",
"user_accepts_direct_messages_from_everybody": "Tutti",
"user_accepts_direct_messages_from_nobody": "Nessuno",
"user_accepts_direct_messages_from_people_i_follow": "Persone che seguo",
"use_one_click_nsfw": "Apri media offuscati con un solo click",
"user_mutes": "Utenti",
"user_profile_default_tab": "Scheda predefinita sul profilo degli utenti",
"user_profiles": "Profili utente",
"user_settings": "Impostazioni utente",
"user_settings": "Impostazioni Utente",
"valid_until": "Valido fino a",
"values": {
"false": "no",
@ -895,141 +847,86 @@
},
"version": {
"backend_version": "Versione backend",
"frontend_version": "Versione frontend",
"frontend_version": "Versione interfaccia",
"title": "Versione"
},
"virtual_scrolling": "Velocizza rendering sequenze",
"word_filter": "Filtro per parola",
"wordfilter": "Filtro per parola"
},
"settings_profile": {
"creating": "Creazione del nuovo profilo di impostazioni \"{profile}\"…",
"synchronization_error": "Non è stato possibile sincronizzare le impostazioni: {err}",
"synchronized": "Impostazioni sincronizzate!",
"synchronizing": "Sincronizzazione del profilo di impostazioni \"{profile}\"…"
"virtual_scrolling": "Velocizza l'elaborazione delle sequenze",
"word_filter": "Parole filtrate"
},
"status": {
"ancestor_follow": "Vedi {numReplies} altra risposta sotto questo post | Vedi {numReplies} altre risposte sotto questo post",
"ancestor_follow_with_icon": "{icon} {text}",
"attachment_stop_flash": "Ferma Flash player",
"bookmark": "Aggiungi segnalibro",
"collapse_attachments": "Riduci allegati",
"copy_link": "Copia collegamento al post",
"delete": "Elimina post",
"delete_confirm": "Vuoi davvero eliminare questo post?",
"delete_confirm_accept_button": "Sì, eliminalo",
"delete_confirm_cancel_button": "No, tienilo",
"delete_confirm_title": "Conferma eliminazione",
"edit": "Modifica",
"edit_history": "Cronologia modifiche",
"edit_history_modal_title": "Modificato {historyCount} volta | Modificato {historyCount} volte",
"edited_at": "Modificato {time}",
"copy_link": "Copia collegamento",
"delete": "Elimina messaggio",
"delete_confirm": "Vuoi veramente eliminare questo messaggio?",
"expand": "Espandi",
"external_source": "Fonte originale",
"external_source": "Vai all'origine",
"favorites": "Preferiti",
"hide_attachment": "Nascondi allegato",
"hide_content": "Nascondi contenuto",
"hide_full_subject": "Nascondi avvertenza sul contenuto intera",
"many_attachments": "Il post ha {number} allegato | Il post ha {number} allegati",
"hide_content": "Nascondi contenuti",
"hide_full_subject": "Nascondi oggetto intero",
"mentions": "Menzioni",
"move_down": "Muovi allegato a destra",
"move_up": "Muovi allegato a sinistra",
"mute_conversation": "Silenzia conversazione",
"nsfw": "NSFW",
"open_gallery": "Apri galleria",
"override_translation_source_language": "Sovrascrivi lingua di origine",
"pin": "Fissa in cima al profilo",
"pinned": "Fissato",
"nsfw": "DISDICEVOLE",
"pin": "Intesta al profilo",
"pinned": "Intestato",
"plus_more": "+{number} altri",
"redraft": "Elimina e correggi",
"redraft_confirm": "Vuoi davvero eliminare e correggere questo post? Le interazioni al post originale non saranno mantenute.",
"redraft_confirm_accept_button": "Sì, elimina e correggi",
"redraft_confirm_cancel_button": "No, tieni l'originale",
"redraft_confirm_title": "Conferma elimina e correggi",
"remove_attachment": "Rimuovi allegato",
"repeat_confirm": "Vuoi davvero condividere questo post?",
"repeat_confirm_accept_button": "Sì, condividilo",
"repeat_confirm_cancel_button": "No, non condividere",
"repeat_confirm_title": "Conferma condivisione",
"repeats": "Condivisioni",
"repeats": "Condivisi",
"replies_list": "Risposte:",
"replies_list_with_others": "Mostra {numReplies} altra risposta | Mostra {numReplies} altre risposte",
"reply_to": "In risposta a",
"show_all_attachments": "Mostra tutti gli allegati",
"show_all_conversation": "Mostra conversazione intera ({numStatus} altro post) | Mostra conversazione intera ({numStatus} altri post)",
"show_all_conversation_with_icon": "{icon} {text}",
"show_attachment_description": "Anteprima descrizione (apri l'allegato per la descrizione intera)",
"show_attachment_in_modal": "Mostra allegato in una finestra",
"show_content": "Mostra contenuto",
"show_full_subject": "Mostra tutta l'avvertenza sul contenuto",
"show_only_conversation_under_this": "Mostra solo le risposte a questo post",
"status_deleted": "Questo post è stato eliminato",
"status_unavailable": "Post non disponibile",
"thread_follow": "Visualizza {numStatus} altra risposta | Visualizza {numStatus} altre risposte",
"thread_follow_with_icon": "{icon} {text}",
"thread_hide": "Nascondi questa conversazione",
"thread_muted": "Conversazione silenziata",
"show_content": "Mostra contenuti",
"show_full_subject": "Mostra oggetto intero",
"status_deleted": "Questo messagio è stato cancellato",
"status_unavailable": "Messaggio non disponibile",
"thread_muted": "Discussione silenziata",
"thread_muted_and_words": ", contiene:",
"thread_show": "Mostra questa conversazione",
"thread_show_full": "Mostra {numStatus} risposta | Mostra tutte e {numStatus} le risposte",
"thread_show_full_with_icon": "{icon} {text}",
"translate": "Traduci",
"translated_from": "Tradotto da {language}",
"unbookmark": "Rimuovi segnalibro",
"unmute_conversation": "Desilenzia conversazione",
"unpin": "Rimuovi dalla cima del profilo",
"unmute_conversation": "Riabilita conversazione",
"unpin": "De-intesta",
"you": "(Tu)"
},
"time": {
"in_future": "fra {0}",
"in_past": "{0} fa",
"now": "proprio adesso",
"now_short": "ora",
"now": "adesso",
"now_short": "adesso",
"unit": {
"days": "{0} giorno | {0} giorni",
"days": "{0} giorni",
"days_short": "{0} g",
"hours": "{0} ora | {0} ore",
"hours_short": "{0} ora | {0} ore",
"minutes": "{0} minuto | {0} minuti",
"hours": "{0} ore",
"hours_short": "{0} h",
"minutes": "{0} minuti",
"minutes_short": "{0} min",
"months": "{0} mese | {0} mesi",
"months_short": "{0} mese | {0} mesi",
"seconds": "{0} secondo | {0} secondi",
"months": "{0} mesi",
"months_short": "{0} mes",
"seconds": "{0} secondi",
"seconds_short": "{0} sec",
"weeks": "{0} settimana | {0} settimane",
"weeks_short": "{0} sett",
"years": "{0} anno | {0} anni",
"weeks": "{0} settimane",
"weeks_short": "{0} stm",
"years": "{0} anni",
"years_short": "{0} a"
}
},
"timeline": {
"collapse": "Riduci",
"collapse": "Ripiega",
"conversation": "Conversazione",
"error": "Errore nel caricare la sequenza: {0}",
"follow_tag": "Segui hashtag",
"load_older": "Carica post precedenti",
"no_more_statuses": "Non ci sono altri post",
"no_retweet_hint": "Il messaggio è «solo per follower» o «diretto», quindi non può essere condiviso",
"no_statuses": "Nessun post",
"load_older": "Carica messaggi precedenti",
"no_more_statuses": "Fine dei messaggi",
"no_retweet_hint": "Il messaggio è diretto o solo per seguaci e non può essere condiviso",
"no_statuses": "Nessun messaggio",
"reload": "Ricarica",
"repeated": "ha condiviso",
"show_new": "Mostra nuovi",
"socket_broke": "Connessione tempo reale interrotta: CloseEvent codice {0}",
"socket_broke": "Connessione tempo reale interrotta: codice {0}",
"socket_reconnected": "Connesso in tempo reale",
"unfollow_tag": "Smetti di seguire hashtag",
"up_to_date": "Aggiornato"
},
"toast": {
"no_translation_target_set": "Nessuna lingua finale di traduzione impostata: la traduzione potrebbe fallire. Imposta una lingua finale di traduzione nelle tue impostazioni."
},
"tool_tip": {
"accept_follow_request": "Accetta richiesta di follow",
"add_reaction": "Aggiungi reazione",
"accept_follow_request": "Accetta seguace",
"add_reaction": "Reagisci",
"bookmark": "Aggiungi segnalibro",
"favorite": "Rendi preferito",
"media_upload": "Carica media",
"quote": "Cita",
"reject_follow_request": "Rifiuta richiesta di follow",
"favorite": "Gradisci",
"media_upload": "Carica allegati",
"reject_follow_request": "Rifiuta seguace",
"repeat": "Condividi",
"reply": "Rispondi",
"user_settings": "Impostazioni utente"
@ -1037,7 +934,7 @@
"upload": {
"error": {
"base": "Caricamento fallito.",
"default": "Riprova più tardi",
"default": "Riprova in seguito",
"file_too_big": "File troppo pesante [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"message": "Caricamento fallito: {0}"
},
@ -1051,115 +948,79 @@
},
"user_card": {
"admin_menu": {
"activate_account": "Riattiva account",
"deactivate_account": "Disattiva account",
"delete_account": "Elimina account",
"activate_account": "Attiva profilo",
"deactivate_account": "Disattiva profilo",
"delete_account": "Elimina profilo",
"delete_user": "Elimina utente",
"delete_user_data_and_deactivate_confirmation": "Questo eliminerà permanentemente i dati dall'account e lo disattiverà. Sei sicuro al 100%?",
"disable_any_subscription": "Proibisci a tutti di seguire l'utente",
"disable_remote_subscription": "Proibisci ad istanze remote di seguire l'utente",
"force_nsfw": "Marca tutti i post come NSFW",
"force_unlisted": "Rendi tutti i post «non in elenco»",
"grant_admin": "Rendi amministratore",
"grant_moderator": "Rendi moderatore",
"disable_any_subscription": "Rendi utente non seguibile",
"disable_remote_subscription": "Blocca i tentativi di seguirlo da altre stanze",
"force_nsfw": "Oscura tutti i messaggi",
"force_unlisted": "Nascondi tutti i messaggi",
"grant_admin": "Crea Amministratore",
"grant_moderator": "Crea Moderatore",
"moderation": "Moderazione",
"quarantine": "Impedisci la federazione dei post degli utenti",
"revoke_admin": "Rimuovi da amministratore",
"revoke_moderator": "Rimuovi da moderatore",
"sandbox": "Rendi tutti i messaggi \"solo per follower\"",
"strip_media": "Rimuovi media dai messaggi"
"quarantine": "I messaggi non arriveranno alle altre stanze",
"revoke_admin": "Divesti Amministratore",
"revoke_moderator": "Divesti Moderatore",
"sandbox": "Rendi tutti i messaggi solo per seguaci",
"strip_media": "Rimuovi ogni allegato ai messaggi"
},
"approve": "Accetta",
"approve_confirm": "Sei sicuro di voler permettere a questo utente di seguirti?",
"approve_confirm_accept_button": "Sì, accetta",
"approve_confirm_cancel_button": "No, annulla",
"approve_confirm_title": "Accetta richiesta di follow",
"approve": "Approva",
"block": "Blocca",
"block_confirm": "Sei sicuro di voler bloccare {user}?",
"block_confirm_accept_button": "Sì, blocca",
"block_confirm_cancel_button": "No, non bloccare",
"block_confirm_title": "Blocca utente",
"block_progress": "Blocco…",
"blocked": "Bloccato!",
"blocks_you": "Ti blocca!",
"bot": "Bot",
"deactivated": "Disattivato",
"deny": "Rifiuta",
"deny_confirm": "Sei sicuro di voler rifiutare la richiesta di follow di questo utente?",
"deny_confirm_accept_button": "Sì, rifiuta",
"deny_confirm_cancel_button": "No, annulla",
"deny_confirm_title": "Rifiuta richiesta di follow",
"domain_muted": "Sblocca dominio",
"deny": "Nega",
"edit_profile": "Modifica profilo",
"favorites": "Preferiti",
"follow": "Segui",
"follow_cancel": "Annulla richiesta",
"follow_progress": "Richiedo…",
"follow_sent": "Richiesta inviata!",
"follow_tag": "Segui l'hashtag",
"follow_unfollow": "Smetti di seguire",
"followed_tags": "Hashtag seguiti",
"followed_users": "Utenti seguiti",
"followees": "Seguiti",
"followers": "Follower",
"following": "Seguito!",
"follow_unfollow": "Disconosci",
"followees": "Segue",
"followers": "Seguaci",
"following": "Seguìto!",
"follows_you": "Ti segue!",
"hidden": "Nascosto",
"hide_repeats": "Nascondi condivisioni",
"highlight": {
"disabled": "Nessuno sfondo",
"side": "Striscia laterale",
"solid": "Sfondo monocolore",
"striped": "Sfondo a righe"
"disabled": "Nessun risalto",
"side": "Nastro a lato",
"solid": "Un colore",
"striped": "A righe"
},
"its_you": "Sei tu!",
"media": "Media",
"mention": "Menziona",
"message": "Contatta",
"mute": "Silenzia",
"mute_confirm": "Sei sicuro di voler silenziare {user}?",
"mute_confirm_accept_button": "Sì, silenzia",
"mute_confirm_cancel_button": "No, non silenziare",
"mute_confirm_title": "Silenzia utente",
"mute_domain": "Blocca dominio",
"mute_progress": "Silenziando…",
"mute_progress": "Silenzio…",
"muted": "Silenziato",
"not_following_any_hashtags": "Non stai seguendo nessun hashtag",
"note": "Nota privata",
"per_day": "al giorno",
"remote_follow": "Segui da remoto",
"remove_follower": "Rimuovi follower",
"replies": "Con risposte",
"report": "Segnala",
"requested_by": "Ha chiesto di seguirti",
"show_repeats": "Mostra condivisioni",
"statuses": "Post",
"subscribe": "Iscriviti",
"statuses": "Messaggi",
"subscribe": "Abbònati",
"unblock": "Sblocca",
"unblock_progress": "Sblocco…",
"unfollow_confirm": "Sei sicuro di voler smettere di seguire {user}?",
"unfollow_confirm_accept_button": "Sì, smetti di seguire",
"unfollow_confirm_cancel_button": "No, non smettere di seguire",
"unfollow_confirm_title": "Smetti di seguire l'utente",
"unfollow_tag": "Smetti di seguire l'hashtag",
"unmute": "Desilenzia",
"unmute_progress": "Desilenziamento…",
"unsubscribe": "Disiscriviti"
"unmute": "Riabilita",
"unmute_progress": "Riabilito…",
"unsubscribe": "Disdici"
},
"user_profile": {
"field_validated": "Collegamento verificato",
"profile_does_not_exist": "Spiacente, questo profilo non esiste.",
"profile_loading_error": "Spiacente, c'è stato un errore nel caricamento del profilo.",
"timeline_title": "Sequenza dell'utente"
},
"user_reporting": {
"add_comment_description": "La segnalazione sarà inviata ai moderatori della tua istanza. Puoi fornire una motivazione per cui stai segnalando questo account qui sotto:",
"additional_comments": "Commenti aggiuntivi",
"forward_description": "Il profilo appartiene ad un altro server. Inviare la segnalazione anche a quello?",
"add_comment_description": "La segnalazione sarà inviata ai moderatori della tua stanza. Puoi motivarla qui sotto:",
"additional_comments": "Osservazioni accessorie",
"forward_description": "Il profilo appartiene ad un'altra stanza. Inviare la segnalazione anche a quella?",
"forward_to": "Inoltra a {0}",
"generic_error": "C'è stato un errore nell'elaborazione della tua richiesta.",
"submit": "Invia",
"title": "Segnala {0}"
"title": "Segnalo {0}"
},
"who_to_follow": {
"more": "Altro",

View file

@ -1,7 +1,6 @@
{
"about": {
"bubble_instances": "ローカルバブルインスタンス",
"bubble_instances_description": "かんりしゃがだいひょうするためにえらんだインスタンス",
"mrf": {
"federation": "フェデレーション",
"keyword": {

View file

@ -1,124 +0,0 @@
{
"about": {
"bubble_instances": "Vietiniai burbulo serveriai",
"bubble_instances_description": "Administratorių parinkti serveriai, kurie atstovauja šios serverio vietinę teritoriją",
"mrf": {
"federation": "Federacija",
"keyword": {
"ftl_removal": "Pašalinimas iš „Viso žinomo tinklo“ laiko skalės",
"is_replaced_by": "→",
"keyword_policies": "Raktažodžių politika",
"reject": "Atmesti",
"replace": "Pakeisti"
},
"mrf_policies": "Įjungta MRF politika",
"mrf_policies_desc": "MRF politika valdo serverio federacijos elgseną. Įjungtos toliau nurodytos politikos:",
"simple": {
"accept": "Priimti",
"accept_desc": "Šis serveris priima žinutes tik iš toliau nurodytų serverių:",
"ftl_removal": "Pašalinimas iš „Žinomo tinklo“ laiko skalės",
"ftl_removal_desc": "Šis serveris pašalina šiuos serverius iš „Žinomo tinklo“ laiko skalės:",
"instance": "Serveris",
"media_nsfw": "Medija priverstinai nustatyta kaip jautri",
"media_nsfw_desc": "Šis serveris priverčia nustatyti mediją kaip jautrią toliau nurodytų serverių įrašuose:",
"media_removal": "Medijos pašalinimas",
"media_removal_desc": "Šis serveris pašalina mediją iš toliau nurodytų serverių įrašų:",
"not_applicable": "Nėra",
"quarantine": "Karantinas",
"quarantine_desc": "Šis serveris nesiųs įrašų į toliau nurodytus serverius:",
"reason": "Priežastis",
"reject": "Atmesti",
"reject_desc": "Šis serveris nepriims žinučių iš toliau nurodytų serverių:",
"simple_policies": "Konkretaus serverio politika"
}
},
"staff": "Personalas"
},
"announcements": {
"all_day_prompt": "Tai visos dienos renginys",
"cancel_edit_action": "Atsisakyti",
"close_error": "Užverti",
"delete_action": "Ištrinti",
"edit_action": "Redaguoti",
"end_time_display": "Pasibaigia {time}",
"end_time_prompt": "Pabaigos laikas: ",
"inactive_message": "Šis skelbimas neaktyvus",
"mark_as_read_action": "Žymėti kaip skaitytą",
"page_header": "Skelbimai",
"post_action": "Siųsti",
"post_error": "Klaida: {error}",
"post_form_header": "Skelbti skelbimą"
},
"chats": {
"chats": "Pokalbiai",
"delete": "Ištrinti",
"more": "Daugiau",
"new": "Naujas pokalbis",
"you": "Jūs:"
},
"display_date": {
"today": "Šiandien"
},
"domain_mute_card": {
"mute": "Nutildyti",
"mute_progress": "Nutildoma…",
"unmute": "Atšaukti nutildymą",
"unmute_progress": "Atšaukiamas nutildymas…"
},
"emoji": {
"add_emoji": "Įterpti jaustuką",
"custom": "Pasirinktinis jaustukas",
"emoji": "Jaustukas",
"stickers": "Lipdukai",
"unicode": "Unikodo jaustukas"
},
"exporter": {
"export": "Eksportuoti"
},
"file_type": {
"audio": "Garso įrašas",
"file": "Failas",
"image": "Vaizdas",
"video": "Vaizdo įrašas"
},
"general": {
"more": "Daugiau",
"scope_in_timeline": {
"direct": "Tiesioginis",
"local": "Vietinis šį įrašą gali matyti tik jūsų serveris",
"private": "Tik sekėjams",
"public": "Vieša",
"unlisted": "Neįtrauktas į sąrašą"
},
"show_less": "Rodyti mažiau",
"show_more": "Rodyti daugiau",
"submit": "Pateikti",
"verify": "Patvirtinti"
},
"image_cropper": {
"cancel": "Atšaukti"
},
"importer": {
"submit": "Pateikti"
},
"user_card": {
"follow_tag": "Sekti saitažodį",
"not_following_any_hashtags": "Nesekate jokių saitažodžių.",
"unfollow_confirm_accept_button": "Taip, nebesekti",
"unfollow_confirm_cancel_button": "Ne, nenaikinti sekimą",
"unfollow_confirm_title": "Nebesekti naudotoją",
"unfollow_tag": "Nebesekti saitažodį"
},
"user_reporting": {
"additional_comments": "Papildomi komentarai",
"forward_description": "Paskyra yra iš kito serverio. Siųsti ataskaitos kopiją ir ten?",
"forward_to": "Persiųsti į {0}",
"generic_error": "Įvyko klaida apdorojant jūsų užklausą.",
"submit": "Pateikti",
"title": "Pranešama apie {0}"
},
"who_to_follow": {
"more": "Daugiau",
"who_to_follow": "Ką sekti"
}
}

View file

@ -599,7 +599,7 @@
"links": "Łącza",
"list_aliases_error": "Błąd pobierania aliasów: {error}",
"list_backups_error": "Błąd pobierania listy kopii zapasowych: {error}",
"lock_account_description": "Wymagaj potwierdzenia nowych śledzących",
"lock_account_description": "Spraw, by konto mogli wyświetlać tylko zatwierdzeni obserwujący",
"loop_video": "Zapętlaj filmy",
"loop_video_silent_only": "Zapętlaj tylko filmy bez dźwięku (np. mastodonowe „gify”)",
"mascot": "Maskotka Mastodon FE",
@ -679,7 +679,6 @@
"pad_emoji": "Dodaj odstęp z obu stron emoji podczas dodawania selektorem",
"panelRadius": "Panele",
"pause_on_unfocused": "Wstrzymuj strumieniowanie kiedy karta nie jest aktywna",
"permit_followback_description": "Automatycznie potwierdź śledzenie przez użytkowników którch już śledzisz",
"play_videos_in_modal": "Odtwarzaj filmy bezpośrednio w przeglądarce mediów",
"post_look_feel": "Wygląd wpisów",
"post_status_content_type": "Domyślny typ zawartości wpisów",
@ -1149,7 +1148,7 @@
"followed_users": "Śledzeni użytkownicy",
"followees": "Obserwowani",
"followers": "Obserwujący",
"following": "Obserwujesz!",
"following": "Obserwowany!",
"follows_you": "Obserwuje cię!",
"hidden": "Ukryte",
"hide_repeats": "Ukryj powtórzenia",

View file

@ -129,131 +129,6 @@
"generic_error": "Bir hata oluştu",
"loading": "Yükleniyor…",
"more": "Daha",
"optional": "Seçenek",
"peek": "Göz at",
"retry": "Tekrar deneyin",
"role": {
"admin": "Yönetici",
"moderator": "Moderatör"
},
"scope_in_timeline": {
"direct": "Doğrudan",
"local": "Yerel - bu gönderiyi yalnızca sizin örneğiniz görebilir",
"private": "Yalnızca takipçiler",
"public": "Herkese açık",
"unlisted": "Listelenmemiş"
},
"show_less": "Daha az göster",
"show_more": "Daha fazla göster",
"submit": "Gönder",
"verify": "Doğrulama"
},
"image_cropper": {
"cancel": "İptal",
"crop_picture": "Resmi kırp",
"save": "Kaydet",
"save_without_cropping": "Kırpmadan kaydet"
},
"importer": {
"error": "Bu dosya içe aktarılırken bir hata oluştu.",
"submit": "Gönder",
"success": "Başarıyla içe aktarıldı."
},
"interactions": {
"favs_repeats": "Tekrarlar ve favoriler",
"follows": "Yeni takipler",
"load_older": "Eski etkileşimleri yükle",
"moves": "Kullanıcı taşıma"
},
"languages": {
"ar": "Arabic",
"az": "Azerbaycan Türkçesi",
"bg": "Bulgarian",
"cs": "Czech",
"da": "Danish",
"de": "German",
"el": "Greek",
"en": "English",
"eo": "Esperanto",
"es": "Spanish",
"fa": "Persian",
"fi": "Finnish",
"fr": "French",
"ga": "Irish",
"he": "Hebrew",
"hi": "Hindi",
"hu": "Hungarian",
"id": "Indonesian",
"it": "Italian",
"ja": "Japanese",
"ko": "Korean",
"lt": "Lithuanian",
"lv": "Latvian",
"nl": "Dutch",
"pl": "Polish",
"pt": "Portuguese",
"ru": "Russian",
"sk": "Slovak",
"sv": "Swedish",
"tr": "Türkçe",
"translated_from": {
"ar": "@:languages.ar adresinden çevrildi",
"az": "@:languages.az adresinden çevrildi",
"bg": "@:languages.bg adresinden çevrildi",
"cs": "@:languages.cs adresinden çevrildi",
"da": "@:languages.da adresinden çevrildi",
"de": "@:languages.de adresinden çevrildi",
"el": "@:languages.el adresinden çevrildi",
"en": "@:languages.en adresinden çevrildi",
"eo": "@:languages.eo adresinden çevrildi",
"es": "@:languages.es adresinden çevrildi",
"fa": "@:languages.fa adresinden çevrildi",
"fi": "@:languages.fi adresinden çevrildi",
"fr": "@:languages.fr adresinden çevrildi",
"ga": "@:languages.ga adresinden çevrildi",
"he": "@:languages.he adresinden çevrildi",
"hi": "@:languages.hi adresinden çevrildi",
"hu": "@:languages.hu adresinden çevrildi",
"id": "@:languages.id adresinden çevrildi",
"it": "@:languages.it adresinden çevrildi",
"ja": "@:languages.ja adresinden çevrildi",
"ko": "@:languages.ko adresinden çevrildi",
"lt": "@:languages.lt adresinden çevrildi",
"lv": "@:languages.lv adresinden çevrildi",
"nl": "@:languages.nl adresinden çevrildi",
"pl": "@:languages.pl adresinden çevrildi",
"pt": "@:languages.pt adresinden çevrildi",
"ru": "@:languages.ru adresinden çevrildi",
"sk": "@:languages.sk adresinden çevrildi",
"sv": "@:languages.sv adresinden çevrildi",
"tr": "@:languages.tr adresinden çevrildi",
"uk": "@:languages.uk adresinden çevrildi",
"zh": "@:languages.zh adresinden çevrildi"
},
"uk": "Ukrainian",
"zh": "Chinese"
},
"lists": {
"create": "Oluştur",
"delete": "Listeyi sil",
"following_only": "Takip Etmeyi Sınırla",
"lists": "Listeler",
"new": "Yeni Liste",
"save": "Değişiklikleri kaydet",
"search": "Kullanıcıları ara",
"title": "Liste başlığı"
},
"login": {
"authentication_code": "Kimlik doğrulama kodu",
"description": "OAuth ile oturum açın",
"enter_recovery_code": "Bir kurtarma kodu girin",
"enter_two_factor_code": "İki faktörlü bir kod girin",
"heading": {
"recovery": "İki faktörlü kurtarma",
"totp": "İki faktörlü kimlik doğrulama"
},
"hint": "Tartışmaya katılmak için giriş yapın",
"login": "Giriş yap",
"logout": ıkış yap"
"optional": "Seçenek"
}
}

View file

@ -492,7 +492,6 @@
"cGreen": "绿色(转发)",
"cOrange": "橙色(喜欢)",
"cRed": "红色(取消)",
"center_align_bio": "使用户简介中的文本居中",
"change_email": "更改邮箱",
"change_email_error": "更改你的邮箱时发生错误。",
"change_password": "更改密码",
@ -503,7 +502,6 @@
"checkboxRadius": "复选框",
"collapse_subject": "折叠带内容警告的帖文",
"columns": "分栏",
"compact_user_info": "空间充足时显示紧凑用户信息",
"composing": "写作",
"confirm_dialogs": "需要确认当:",
"confirm_dialogs_approve_follow": "接受关注请求",
@ -684,7 +682,6 @@
"pause_on_unfocused": "在离开页面时暂停时间线推送",
"permit_followback_description": "自动批准已关注用户的关注请求",
"play_videos_in_modal": "在弹出框内播放视频",
"post_language": "默认发布的语言",
"post_look_feel": "文章的样子跟感受",
"post_status_content_type": "默认发布的内容类型",
"posts": "帖文",
@ -950,7 +947,6 @@
"title": "版本"
},
"virtual_scrolling": "优化时间线渲染",
"widen_timeline": "加宽时间线以填充水平空间",
"word_filter": "词语过滤",
"wordfilter": "词语过滤器"
},

View file

@ -4,20 +4,9 @@ import { each, get, set, cloneDeep } from 'lodash'
let loaded = false
// use this to avoid cloning and persisitng private runtime state
// (runtime state can be reconstructed and may include non-cloneable objects like functions)
const clonePublicKeys = (state) => {
const clonedState = {}
for (const key in state) {
if (!key.startsWith('__'))
set(clonedState, key, cloneDeep(state[key]))
}
return clonedState
}
const defaultReducer = (state, paths) => (
paths.length === 0 ? clonePublicKeys(state) : paths.reduce((substate, path) => {
set(substate, path, clonePublicKeys(get(state, path)))
paths.length === 0 ? state : paths.reduce((substate, path) => {
set(substate, path, get(state, path))
return substate
}, {})
)
@ -30,7 +19,7 @@ const saveImmedeatelyActions = [
'setOption',
'setClientData',
'setToken',
'clearTokens',
'clearToken',
'emojiUsed',
]
@ -81,7 +70,7 @@ export default function createPersistedState ({
subscriber(store)((mutation, state) => {
try {
if (saveImmedeatelyActions.includes(mutation.type)) {
setState(key, reducer(state, paths), storage)
setState(key, reducer(cloneDeep(state), paths), storage)
.then(success => {
if (typeof success !== 'undefined') {
if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {

View file

@ -1,28 +0,0 @@
const SCOPE_LEVELS = {
'direct': 0,
'private': 1,
'unlisted': 2,
'local': 3,
'public': 3
}
export default {
negotiate: (defaultScope, maxScope) => {
if (!maxScope)
return defaultScope;
if (maxScope === 'local')
return defaultScope === 'direct' ? defaultScope : 'local';
if (SCOPE_LEVELS[defaultScope] <= SCOPE_LEVELS[maxScope])
return defaultScope;
else
return maxScope;
},
isSubScope: (original, subscope) => {
if (original === 'local')
return (subscope === 'direct' || subscope === original);
return SCOPE_LEVELS[subscope] <= SCOPE_LEVELS[original];
}
}

View file

@ -7,7 +7,6 @@ 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 dmConversationsModule from './modules/dm_conversations.js'
import usersModule from './modules/users.js'
import apiModule from './modules/api.js'
import configModule from './modules/config.js'
@ -88,7 +87,6 @@ const persistedStateOptions = {
users: usersModule,
statuses: statusesModule,
lists: listsModule,
dmConversations: dmConversationsModule,
api: apiModule,
config: configModule,
serverSideConfig: serverSideConfigModule,

View file

@ -229,7 +229,7 @@ const api = {
// Timelines
startFetchingTimeline(
store,
{ timeline = "friends", tag = false, userId = false, listId = false, conversationId = false },
{ timeline = "friends", tag = false, userId = false, listId = false },
) {
if (store.state.fetchers[timeline]) return;
@ -238,7 +238,6 @@ const api = {
store,
userId,
listId,
conversationId,
tag,
});
store.commit("addFetcher", { fetcherName: timeline, fetcher });
@ -348,20 +347,6 @@ const api = {
store.commit("removeFetcher", { fetcherName: "reports", fetcher });
},
// info of current user; e.g. count of pending follow requests
startFetchingCurrentUserInfo(store, { id }) {
if (store.state.fetchers["current_user"]) return;
const fetcher = store.state.backendInteractor.startFetchingCurrentUserInfo({
store, id
});
store.commit("addFetcher", { fetcherName: "current_user", fetcher });
},
stopFetchingCurrentUserInfo(store) {
const fetcher = store.state.fetchers.current_user;
if (!fetcher) return;
store.commit("removeFetcher", { fetcherName: "current_user", fetcher });
},
getSupportedTranslationlanguages(store) {
store.state.backendInteractor
.getSupportedTranslationlanguages({ store })

View file

@ -22,7 +22,7 @@ export const multiChoiceProperties = [
export const defaultState = {
profile: 'default',
profileVersion: 0, // internal fe copy of server-side version
profileVersion: 0,
expertLevel: 0, // used to track which settings to show and hide
colors: {},
theme: undefined,
@ -56,8 +56,6 @@ export const defaultState = {
autohideFloatingPostButton: false,
pauseOnUnfocused: true,
displayPageBackgrounds: true,
centerAlignBio: false,
compactUserInfo: true,
stopGifs: undefined,
replyVisibility: 'all',
thirdColumnMode: 'notifications',
@ -79,7 +77,6 @@ export const defaultState = {
hideScopeNotice: false,
useStreamingApi: false,
sidebarRight: undefined, // instance default
widenTimeline: undefined, // instance default
subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default
postContentType: undefined, // instance default
@ -93,7 +90,6 @@ export const defaultState = {
modalOnLogout: undefined, // instance default
modalOnApproveFollow: undefined, // instance default
modalOnDenyFollow: undefined, // instance default
modalOnDeleteDMConversation: undefined, // instance default
playVideosInModal: false,
useOneClickNsfw: false,
useContainFit: true,
@ -131,52 +127,8 @@ export const instanceDefaultProperties = Object.entries(defaultState)
.filter(([key, value]) => value === undefined)
.map(([key, value]) => key)
function updateLocalSettings(store, settingEntries, version = null) {
if (version == null)
version = store.state.profileVersion
settingEntries.forEach(([name, value]) => {
if (store.state[name] !== value) {
store.dispatch('setOption', { name, value })
}
})
// Set this at the end to override any potentially stored profileVersion
store.commit('setOption', { name: 'profileVersion', value: version })
}
/**
* Parses the raw mute-word string into an array of mute rules
* containing the original filter defintion and a predicator function
* and optionally (for optimisation purposes when checking against many mute rules)
* the lower-cased version of the text as arguments.
*
* muteRule = { name: string, predicate: (string, string) => boolean }
*/
const parseMuteWords = (rawList) => {
const regexStart = '/'
const regexEnd = '/'
return rawList.map((word) => {
let predicate
if (word.startsWith(regexStart) && word.endsWith(regexEnd)) {
// case-insenstitive by default, but regex modifiers can locally reenable case sensitivity
const regex = new RegExp(word.slice(regexStart.length, -regexEnd.length), 'i')
predicate = (text, _textLowCased) => regex.test(text)
} else {
const muteWord = word.toLowerCase()
predicate = (text, textLowCased) => (textLowCased || text?.toLowerCase())?.includes(muteWord)
}
return { name: word, predicate: predicate }
})
}
const settingParsers = {
muteWords: parseMuteWords,
}
const config = {
state: { ...defaultState, '__parsed_cache': {} },
state: { ...defaultState },
getters: {
defaultConfig (state, getters, rootState, rootGetters) {
const { instance } = rootState
@ -191,32 +143,14 @@ const config = {
const { defaultConfig } = rootGetters
return {
...defaultConfig,
// Do not override with undefined and exclude private caches
...Object.fromEntries(Object.entries(state).filter(([k, v]) => v !== undefined && k !== '__parsed_cache'))
}
},
parsedConfigVal (state, getters, rootState, rootGetters) {
return (key) => {
let cached = state['__parsed_cache'][key]
if (cached !== undefined) return cached;
const parser = settingParsers[key]
if (!parser) return undefined;
// we can't use mergedConfig yet here
const rawVal = state[key] || rootState.instance?.[key] || defaultState[key];
const parsed = parser(rawVal);
state['__parsed_cache'][key] = parsed
return parsed
// Do not override with undefined
...Object.fromEntries(Object.entries(state).filter(([k, v]) => v !== undefined))
}
}
},
mutations: {
setOption (state, { name, value }) {
state[name] = value
if (settingParsers.hasOwnProperty(name)) {
state['__parsed_cache'][name] = undefined
}
},
setHighlight (state, { user, color, type }) {
const data = this.state.config.highlight[user]
@ -237,9 +171,8 @@ const config = {
timeout: 5000
}
store.dispatch('pushGlobalNotice', notice)
let {'__parsed_cache': _, ...currentSettings} = store.state
store.rootState.api.backendInteractor.saveSettingsProfile({
settings: currentSettings, profileName: store.state.profile, version: store.state.profileVersion
settings: store.state, profileName: store.state.profile, version: store.state.profileVersion
}).then(() => {
store.dispatch('removeGlobalNotice', notice)
store.dispatch('pushGlobalNotice', {
@ -265,17 +198,19 @@ const config = {
store.dispatch('listSettingsProfiles')
})
},
loadSettings (store, data) {
loadSettings ({ dispatch }, data) {
const knownKeys = new Set(Object.keys(defaultState))
const presentKeys = new Set(Object.keys(data))
const intersection = new Set()
for (let elem of presentKeys) {
if (knownKeys.has(elem)) {
intersection.add(elem)
}
}
// Limit to supported properties
const newSettingEntries =
Object.entries(data)
.filter(([key, value]) => knownKeys.has(key))
// disregard stored profileVersion; sync afterwards increases previous version
updateLocalSettings(store, newSettingEntries, null)
store.dispatch('syncSettings')
intersection.forEach(
name => dispatch('setOption', { name, value: data[name] })
)
},
setHighlight ({ commit, dispatch }, { user, color, type }) {
commit('setHighlight', { user, color, type })
@ -309,7 +244,12 @@ const config = {
.then(({ settings, version }) => {
console.log('found settings version', version)
if (forceUpdate || (version > store.state.profileVersion)) {
updateLocalSettings(store, Object.entries(settings), version)
store.commit('setOption', { name: 'profileVersion', value: version })
Object.entries(settings).forEach(([name, value]) => {
if (store.state[name] !== value) {
store.dispatch('setOption', { name, value })
}
})
} else {
console.log('settings are up to date')
}

View file

@ -1,106 +0,0 @@
import { remove, find } from 'lodash'
export const defaultState = {
allDMConversations: [],
dmConversationsPagination: {}
}
export const getters = {
getDMConversationById: (state) => (id) => {
return find(state.allDMConversations, { id })
}
}
const mutations = {
clearDMConversations(state) {
state.allDMConversations = []
state.dmConversationsPagination = undefined
},
saveDMConversationPagination(state, pagination) {
state.dmConversationsPagination = pagination
},
addDMConversations(state, list) {
state.allDMConversations.push(...list)
},
updateDMConversation(state, conv) {
const idx = state.allDMConversations.findIndex(c => c.id == conv.id)
if (idx >= 0)
state.allDMConversations[idx] = conv
else
state.allDMConversations.push(conv)
},
updateDMConversations(state, list) {
for (const conv of list) {
const idx = state.allDMConversations.findIndex(c => c.id == conv.id)
if (idx >= 0)
state.allDMConversations[idx] = conv
else
state.allDMConversations.push(conv)
}
},
deleteDMConversation(state, id) {
remove(state.allDMConversations, { id })
}
}
const actions = {
fetchDMConversationList({ rootState, commit }) {
const savedPagination = rootState.dmConversations.dmConversationsPagination
return rootState.api.backendInteractor
.fetchDMConversationList({ pagination: savedPagination })
.then(({ data, pagination }) => {
commit('addDMConversations', data)
commit('saveDMConversationPagination', pagination)
return data
})
},
fetchDMConversationDetails({ rootState, commit }, { id }) {
return rootState.api.backendInteractor
.fetchDMConversationDetails({ id })
.then(data => {
commit('updateDMConversation', data)
return data
})
},
setDMConversationDetails({ rootState, commit }, { id, ...params }) {
return rootState.api.backendInteractor
.setDMConversationDetails({ id, ...params })
.then(data => {
commit('updateDMConversation', data)
return data
})
},
clearDMConversations({ commit }) {
commit('clearDMConversations')
},
markDMConversationAsRead({ rootState, commit }, { id }) {
rootState.api.backendInteractor
.markDMConversationAsRead({ id })
.then(data => {
commit('updateDMConversation', data)
commit('decrementUnreadDMConversationsCount')
})
},
markAllDMConversationsAsRead({ rootState, commit }) {
rootState.api.backendInteractor
.markAllDMConversationsAsRead()
.then(data => {
commit('updateDMConversations', data)
commit('clearUnreadDMConversationsCount')
})
},
deleteDMConversation({ rootState, commit }, { id }) {
rootState.api.backendInteractor
.deleteDMConversation({ id })
.then(data => commit('deleteDMConversation', id))
}
}
const dmConversations = {
state: defaultState,
mutations,
actions,
getters
}
export default dmConversations

View file

@ -47,7 +47,6 @@ const defaultState = {
modalOnLogout: true,
modalOnApproveFollow: false,
modalOnDenyFollow: false,
modalOnDeleteDMConversation: true,
loginMethod: 'password',
logo: '/static/logo.svg',
logoMargin: '.2em',
@ -62,7 +61,6 @@ const defaultState = {
showNavShortcuts: true,
showWiderShortcuts: true,
sidebarRight: false,
widenTimeline: false,
subjectLineBehavior: 'email',
theme: 'pleroma-dark',
virtualScrolling: true,
@ -75,8 +73,6 @@ const defaultState = {
conversationOtherRepliesButton: 'below',
conversationTreeFadeAncestors: false,
maxDepthInThread: 6,
backendCommitUrl: 'https://akkoma.dev/AkkomaGang/akkoma/commit/',
frontendCommitUrl: 'https://akkoma.dev/AkkomaGang/pleroma-fe/commit/',
// Nasty stuff
customEmoji: [],
@ -186,13 +182,10 @@ const instance = {
const result = await res.json()
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
const emoji = Object.entries(values).map(([key, value]) => {
let imageUrl = value.image_url
if (typeof imageUrl == 'string' && imageUrl.startsWith('/'))
imageUrl = state.server + imageUrl;
const imageUrl = value.image_url
return {
displayText: key,
imageUrl: imageUrl ? imageUrl : value,
imageUrl: imageUrl ? state.server + imageUrl : value,
tags: imageUrl ? value.tags.sort((a, b) => a > b ? 1 : 0) : ['utf'],
replacement: `:${key}: `
}

View file

@ -3,7 +3,8 @@ const oauth = {
clientId: false,
clientSecret: false,
/* App token is authentication for app without any user, used mostly for
* MastoAPI's registration of new users and unnecessary otherwise
* MastoAPI's registration of new users, stored so that we can fall back to
* it on logout
*/
appToken: false,
/* User token is authentication for app with user, this is for every calls
@ -22,9 +23,8 @@ const oauth = {
setToken (state, token) {
state.userToken = token
},
clearTokens (state) {
clearToken (state) {
state.userToken = false
state.appToken = false
// state.token is userToken with older name, coming from persistent state
// let's clear it as well, since it is being used as a fallback of state.userToken
delete state.token

View file

@ -1,7 +1,5 @@
import { merge } from 'lodash'
const POLL_UPDATE_FREQUENCY = 150_000;
const polls = {
state: {
// Contains key = id, value = number of trackers for this poll
@ -14,9 +12,6 @@ const polls = {
// Make expired-state change trigger re-renders properly
poll.expired = Date.now() > Date.parse(poll.expires_at)
if (existingPoll) {
if (poll.expired) {
state.trackedPolls[poll.id] = 0
}
state.pollsObject[poll.id] = merge(existingPoll, poll)
} else {
state.pollsObject[poll.id] = poll
@ -49,16 +44,13 @@ const polls = {
if (rootState.polls.trackedPolls[pollId]) {
dispatch('updateTrackedPoll', pollId)
}
}, POLL_UPDATE_FREQUENCY)
}, 30 * 1000)
commit('mergeOrAddPoll', poll)
})
},
trackPoll ({ rootState, commit, dispatch }, pollId) {
if (rootState.polls.pollsObject[pollId]?.expired)
return;
if (!rootState.polls.trackedPolls[pollId]) {
setTimeout(() => dispatch('updateTrackedPoll', pollId), POLL_UPDATE_FREQUENCY)
setTimeout(() => dispatch('updateTrackedPoll', pollId), 30 * 1000)
}
commit('trackPoll', pollId)
},

View file

@ -61,9 +61,9 @@ export const defaultState = () => ({
publicAndExternal: emptyTl(),
friends: emptyTl(),
tag: emptyTl(),
dms: emptyTl(),
bookmarks: emptyTl(),
list: emptyTl(),
dmConv: emptyTl(),
bubble: emptyTl(),
replies: emptyTl()
}
@ -205,6 +205,14 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
sortTimeline(mentions)
}
}
if (status.visibility === 'direct') {
const dms = state.timelines.dms
mergeOrAdd(dms.statuses, dms.statusesObject, status)
dms.newStatusCount += 1
sortTimeline(dms)
}
}
// Decide if we should treat the status as new for this timeline.
@ -655,10 +663,10 @@ const statuses = {
return rootState.api.backendInteractor.unmuteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status))
},
retweet ({ rootState, commit }, {id, visibility}) {
retweet ({ rootState, commit }, status) {
// Optimistic retweeting...
commit('setRetweeted', { status: {id: id}, value: true })
rootState.api.backendInteractor.retweet({ id: id, visibility: visibility })
commit('setRetweeted', { status, value: true })
rootState.api.backendInteractor.retweet({ id: status.id })
.then(status => commit('setRetweetedConfirm', { status: status.retweeted_status, user: rootState.users.currentUser }))
},
unretweet ({ rootState, commit }, status) {

View file

@ -3,7 +3,6 @@ import { windowWidth, windowHeight } from '../services/window_utils/window_utils
import oauthApi from '../services/new_api/oauth.js'
import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
import { getClientToken } from '../services/new_api/oauth.js'
// TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item, key = 'id') => {
@ -113,17 +112,6 @@ const setNote = (store, { id, note }) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const getAppSecret = async ({ store }) => {
const { rootState, commit } = store
const { oauth, instance } = rootState
return oauthApi.getOrCreateApp({ ...oauth, instance: instance.server, commit })
.then((app) => getClientToken({ ...app, instance: instance.server }))
.then((token) => {
commit('setAppToken', token.access_token)
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
})
}
export const mutations = {
tagUser (state, { user: { id }, tag }) {
const user = state.usersObject[id]
@ -155,10 +143,6 @@ export const mutations = {
state.lastLoginName = user.screen_name
state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength)
},
updateCurrentUser (state, user) {
if (user.id !== state.currentUser?.id) return
state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength)
},
clearCurrentUser (state) {
state.currentUser = false
state.lastLoginName = false
@ -293,14 +277,6 @@ export const mutations = {
state.signUpPending = false
state.signUpErrors = errors
},
clearUnreadDMConversationsCount (store) {
if (store.currentUser.pleroma)
store.currentUser.pleroma.unread_conversation_count = 0
},
decrementUnreadDMConversationsCount (store) {
if (store.currentUser.pleroma?.unread_conversation_count)
store.currentUser.pleroma.unread_conversation_count--
},
decrementFollowRequestsCount (store) {
store.currentUser.follow_requests_count--
},
@ -592,8 +568,6 @@ const users = {
let rootState = store.rootState
try {
// registration can only be done with an app token
await getAppSecret({ store })
let data = await rootState.api.backendInteractor.register(
{ params: { ...userInfo } }
)
@ -629,25 +603,18 @@ const users = {
return oauthApi.getOrCreateApp(data)
.then((app) => {
// Clear both OAuth token (used in every login session)
// and app token (only used by us during registration)
for (const token of [oauth.userToken, oauth.appToken]) {
if (!token) continue
const params = {
app,
instance: data.instance,
token: token
}
oauthApi.revokeToken(params)
const params = {
app,
instance: data.instance,
token: oauth.userToken
}
return oauthApi.revokeToken(params)
})
.then(() => {
store.dispatch('stopFetchingCurrentUserInfo')
store.commit('clearCurrentUser')
store.dispatch('disconnectFromSocket')
store.commit('clearTokens')
store.commit('clearToken')
store.dispatch('stopFetchingTimeline', 'friends')
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
store.dispatch('stopFetchingNotifications')
@ -716,7 +683,6 @@ const users = {
store.dispatch('listSettingsProfiles')
store.dispatch('startFetchingConfig')
store.dispatch('startFetchingAnnouncements')
store.dispatch('startFetchingCurrentUserInfo', { id: user.id })
if (user.role === 'admin' || user.role === 'moderator') {
store.dispatch('startFetchingReports')
}

View file

@ -18,6 +18,7 @@
bottom: 0;
left: 0;
right: 0;
z-index: 5;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
box-shadow: var(--panelShadow);
pointer-events: none;
@ -59,6 +60,7 @@
padding: 0.6em;
height: var(--__panel-heading-height);
line-height: var(--__panel-heading-height-inner);
z-index: 4;
&.-flexible-height {
--__panel-heading-height: auto;
@ -129,7 +131,6 @@
color: var(--panelText);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
z-index: 4;
&::after {
background-color: $fallback--fg;

View file

@ -1,5 +1,5 @@
import { each, map, concat, last, get } from 'lodash'
import { parseStatus, parseSource, parseUser, parseNotification, parseReport, parseAttachment, parseDMConversation, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
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'
@ -50,15 +50,10 @@ const MASTODON_FOLLOWERS_URL = id => `/api/v1/accounts/${id}/followers`
const MASTODON_FOLLOW_REQUESTS_URL = '/api/v1/follow_requests'
const MASTODON_APPROVE_USER_URL = id => `/api/v1/follow_requests/${id}/authorize`
const MASTODON_DENY_USER_URL = id => `/api/v1/follow_requests/${id}/reject`
const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = '/api/v1/timelines/direct'
const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public'
const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home'
const AKKOMA_BUBBLE_TIMELINE_URL = '/api/v1/timelines/bubble'
const MASTODON_USER_CONVERSATIONS_URL = '/api/v1/conversations'
const MASTODON_CONVERSATION_DELETE = id => `/api/v1/conversations/${id}`
const MASTODON_CONVERSATION_MARK_READ = id => `/api/v1/conversations/${id}/read`
const PLEROMA_CONVERSATION_MARK_ALL_READ = '/api/v1/pleroma/conversations/read'
const PLEROMA_CONVERSATION_DETAILS = id => `/api/v1/pleroma/conversations/${id}`
const PLEROMA_CONVERSATION_TIMELINE = id => `/api/v1/pleroma/conversations/${id}/statuses`
const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}`
const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context`
const MASTODON_STATUS_SOURCE_URL = id => `/api/v1/statuses/${id}/source`
@ -129,21 +124,6 @@ let fetch = (url, options) => {
return oldfetch(fullUrl, options)
}
// TODO: integrate directly into above adapting callers as needed
const getJsonIfSuccess = (response, url, options) => {
return new Promise((resolve, reject) => response.json()
.then((json) => {
if (!response.ok) {
return reject(new StatusCodeError(response.status, json, { url, options }, response))
}
return resolve(json)
})
.catch((error) => {
return reject(new StatusCodeError(response.status, error, { url, options }, response))
})
)
}
const promisedRequest = ({ method, url, params, payload, credentials, headers = {} }) => {
const options = {
method,
@ -155,13 +135,7 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers =
}
if (params) {
url += '?' + Object.entries(params)
.map(([key, value]) => {
if (!Array.isArray(value)) {
return (encodeURIComponent(key) + '=' + encodeURIComponent(value))
} else {
return value.map((v) => encodeURIComponent(key) + '[]=' + encodeURIComponent(v)).join('&')
}
})
.map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value))
.join('&')
}
if (payload) {
@ -174,7 +148,19 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers =
}
}
return fetch(url, options)
.then((response) => getJsonIfSuccess(response, url, options))
.then((response) => {
return new Promise((resolve, reject) => response.json()
.then((json) => {
if (!response.ok) {
return reject(new StatusCodeError(response.status, json, { url, options }, response))
}
return resolve(json)
})
.catch((error) => {
return reject(new StatusCodeError(response.status, error, { url, options }, response))
})
)
})
}
const updateNotificationSettings = ({ credentials, settings }) => {
@ -525,80 +511,6 @@ const fetchConversation = ({ id, credentials }) => {
}))
}
const fetchDMConversationList = ({ credentials, pagination: savedPagination }) => {
// We always start fetching from most recent and move back in time
const queryParams = new URLSearchParams()
if (savedPagination?.maxId) {
queryParams.append('max_id', savedPagination.maxId)
}
let url = `${MASTODON_USER_CONVERSATIONS_URL}?${queryParams.toString()}`
let pagination = {}
return fetch(url, { headers: authHeaders(credentials) })
.then((response) => {
pagination = parseLinkHeaderPagination(response.headers.get('Link'), {
flakeId: true
})
return getJsonIfSuccess(response, url, {})
})
.then((data) => {
return {
pagination,
data: data.map(c => parseDMConversation(c))
}
})
}
const fetchDMConversationDetails = ({ id, credentials }) => {
let url = PLEROMA_CONVERSATION_DETAILS(id)
return fetch(url, { headers: authHeaders(credentials) })
.then((response) => getJsonIfSuccess(response, url, {}))
.then((data) => parseDMConversation(data))
}
const setDMConversationDetails = ({id, credentials, ...params }) => {
let url = PLEROMA_CONVERSATION_DETAILS(id)
return promisedRequest({
url: url,
method: 'PATCH',
credentials,
// XXX: backend should start also accepting JSON request bodies as preferred by other endpoints
params: params
})
.then((data) => parseDMConversation(data))
}
const markDMConversationAsRead = ({ id, credentials }) => {
let url = MASTODON_CONVERSATION_MARK_READ(id)
return fetch(url, {
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => getJsonIfSuccess(response, url, {}))
.then((data) => parseDMConversation(data))
}
const markAllDMConversationsAsRead = ({ credentials }) => {
let url = PLEROMA_CONVERSATION_MARK_ALL_READ
return fetch(url, {
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => getJsonIfSuccess(response, url, {}))
.then((data) => data.map(c => parseDMConversation(c)))
}
const deleteDMConversation = ({ id, credentials }) => {
let url = MASTODON_CONVERSATION_DELETE(id)
return fetch(url, {
method: 'DELETE',
headers: authHeaders(credentials)
})
.then((response) => getJsonIfSuccess(response, url, {}))
}
const fetchStatus = ({ id, credentials }) => {
let url = MASTODON_STATUS_URL(id)
return fetch(url, { headers: authHeaders(credentials) })
@ -781,7 +693,6 @@ const fetchTimeline = ({
until = false,
userId = false,
listId = false,
conversationId = false,
tag = false,
withMuted = false,
replyVisibility = 'all'
@ -790,7 +701,7 @@ const fetchTimeline = ({
public: MASTODON_PUBLIC_TIMELINE,
bubble: AKKOMA_BUBBLE_TIMELINE_URL,
friends: MASTODON_USER_HOME_TIMELINE_URL,
dmConv: PLEROMA_CONVERSATION_TIMELINE,
dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
notifications: MASTODON_USER_NOTIFICATIONS_URL,
'publicAndExternal': MASTODON_PUBLIC_TIMELINE,
user: MASTODON_USER_TIMELINE_URL,
@ -814,10 +725,6 @@ const fetchTimeline = ({
url = url(listId)
}
if (timeline === 'dmConv') {
url = url(conversationId)
}
if (since) {
params.push(['since_id', since])
}
@ -915,8 +822,8 @@ const unfavorite = ({ id, credentials }) => {
.then((data) => parseStatus(data))
}
const retweet = ({ id, visibility, credentials }) => {
return promisedRequest({ url: MASTODON_RETWEET_URL(id), method: 'POST', payload: { visibility }, credentials })
const retweet = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_RETWEET_URL(id), method: 'POST', credentials })
.then((data) => parseStatus(data))
}
@ -1081,7 +988,7 @@ const uploadMedia = ({ formData, credentials }) => {
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => getJsonIfSuccess(response, MASTODON_MEDIA_UPLOAD_URL, {}))
.then((data) => data.json())
.then((data) => parseAttachment(data))
}
@ -1681,7 +1588,7 @@ const getFollowedHashtags = ({ credentials, pagination: savedPagination }) => {
const url = `${MASTODON_FOLLOWED_TAGS_URL}?${queryParams.toString()}`
let pagination = {};
return fetch(url, {
headers: authHeaders(credentials),
credentials
}).then((data) => {
pagination = parseLinkHeaderPagination(data.headers.get('Link'), {
flakeId: false
@ -1703,7 +1610,7 @@ const getFollowRequests = ({ credentials, pagination: savedPagination }) => {
const url = `${MASTODON_FOLLOW_REQUESTS_URL}?${queryParams.toString()}`
let pagination = {};
return fetch(url, {
headers: authHeaders(credentials),
credentials
}).then((data) => {
pagination = parseLinkHeaderPagination(data.headers.get('Link'), { flakeId: true });
return data.json()
@ -1839,12 +1746,6 @@ const apiService = {
fetchTimeline,
fetchPinnedStatuses,
fetchConversation,
fetchDMConversationList,
fetchDMConversationDetails,
setDMConversationDetails,
markDMConversationAsRead,
markAllDMConversationsAsRead,
deleteDMConversation,
fetchStatus,
fetchStatusSource,
fetchStatusHistory,

Some files were not shown because too many files have changed in this diff Show more