forked from AkkomaGang/akkoma-fe
Compare commits
172 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6c0d35d29 | |||
|
8f5cf700f8 |
|||
|
|
efe15c98c6 | ||
|
|
a734eda0d9 | ||
|
51caf0430f |
|||
| 48905a4431 | |||
| c465cb0a35 | |||
|
|
affbc240d1 | ||
| a123b41a2f | |||
| 4ab3424508 | |||
| b04e4810f8 | |||
| fc8debd2c4 | |||
|
|
8227c84aa2 | ||
|
|
42595fcb2c | ||
|
|
e3a72827ef | ||
| 34e4928754 | |||
|
|
9bfd3936d6 | ||
|
|
8d8e6d979a | ||
| e52157042d | |||
|
|
9b45a382b0 | ||
|
|
d73d7a2a0d | ||
|
|
252f8c5e2d | ||
|
|
8d24b877e0 | ||
|
|
b3b998fd1f | ||
|
|
2e53ee6536 | ||
|
|
9fdf2d22a7 | ||
|
|
df9bb44e14 | ||
|
|
3d4c79b344 | ||
|
|
0da1a32767 | ||
| 3a20ec5162 | |||
|
|
28d0a30888 | ||
|
|
2fb38a597c | ||
| 2760495b54 | |||
| 2ef333dafc | |||
| 6d260c08c0 | |||
|
|
7456b8b02f | ||
|
|
d83fd8b1cd | ||
|
|
8f948d52f5 | ||
| 9a671325ad | |||
|
|
26a4188620 | ||
|
|
b839a25060 | ||
| 09fba6bada | |||
| 4d85f0a074 | |||
| a1e83062b4 | |||
|
|
4315788019 | ||
|
|
f2c55423fd | ||
| 9bbc68536c | |||
|
|
c5f068d6fa | ||
|
|
98826e8462 | ||
| b13ecbcf6f | |||
| 66b026561f | |||
|
|
c925f7f91b | ||
|
|
c3673eb53a | ||
|
|
f0c149950c | ||
| 3d54c8274f | |||
|
|
f6bf484d4b | ||
| 57a809946c | |||
| 2f64931d5b | |||
|
|
c2db0e66ef | ||
| 762676e105 | |||
| 1fa242232e | |||
|
|
e71da57845 | ||
|
|
877dde80c9 | ||
|
|
f885728ccd | ||
|
|
4cb74a3fbe | ||
|
|
e79916e78e | ||
|
|
3881f87c79 | ||
|
|
82647e8e98 | ||
|
|
539977de9d | ||
| d2995ada16 | |||
|
|
bcd15ef858 | ||
| 900ac68ca6 | |||
|
|
34bbcef83e | ||
| 37ce8352a9 | |||
| f48138c979 | |||
| 5baa2ce40f | |||
|
|
fef531b8a0 | ||
| bf0c137057 | |||
| 5a50ceb3aa | |||
| f08a961199 | |||
| d252e10543 | |||
|
|
7c84854b10 | ||
|
|
ab606c6160 | ||
|
|
38d8a9751a | ||
|
|
2c92467dcd | ||
| bb71635d12 | |||
| e1b4d8f59a | |||
| 2455bb70f3 | |||
| fbc6cd59bc | |||
|
|
873048de2e | ||
|
|
7a4e2a8644 | ||
|
|
a1d92ffd86 | ||
|
|
b60f42b959 | ||
| 55dff3a9bd | |||
| 9ef8effeed | |||
| 9c15db16a6 | |||
|
|
674a816453 | ||
|
|
be2207fa42 | ||
|
|
3f3ea32f81 | ||
|
|
abc6b299e0 | ||
|
|
b8b18c67b1 | ||
| 4cf4b5e2d0 | |||
| d617a9596a | |||
|
|
4734e9668d | ||
| 9787f43343 | |||
| 61bdedc82f | |||
| a4eddc7f1c | |||
| 94c5998593 | |||
|
|
851dd263c0 | ||
|
|
473ba89355 | ||
| 4ce8ffcec1 | |||
| e62b154228 | |||
| e87a9ced61 | |||
| 7245775b27 | |||
| 6373c5a05d | |||
| 2914eaf1ca | |||
| 0bf9cb0660 | |||
| 65cb3b95e0 | |||
| f15b94d566 | |||
| 06ba190e2e | |||
| 2086522d64 | |||
|
|
fa294e0003 | ||
| d3fa5cfad0 | |||
|
|
9552287442 | ||
| 6b7c8f0def | |||
|
|
3386692e26 | ||
| ad6bb47003 | |||
|
|
9838545904 | ||
|
|
868c6e41ac | ||
|
|
b3f25e5d84 | ||
|
|
248509073e | ||
|
|
a7d6235131 | ||
|
|
177d96f977 | ||
|
|
42ba77ebf4 | ||
| 4a50b1273d | |||
| c76dc6d79e | |||
| cb4c581cde | |||
|
|
8231c8f0b6 |
||
|
|
ef242a1ddd | ||
| 35cf3327c8 | |||
|
|
1ae09458c6 | ||
|
|
25681cf5f6 | ||
|
|
6666a273a4 | ||
|
|
3210873d7f | ||
|
|
f5f9949253 | ||
|
|
ba4ae5badb | ||
|
|
56a59e1b55 | ||
|
|
3065416c93 | ||
| 94141dcb3c | |||
|
|
92e278d406 | ||
| 94ed0991bc | |||
| e955eb4503 | |||
| c39d9fa64b | |||
| a74a631793 | |||
| 2e83ccefdc | |||
| cf11b2523e | |||
| 85001814a2 | |||
| c902219997 | |||
| 2e2e87db75 | |||
| b2af067fd3 | |||
| 754cd2fa57 | |||
| 31055fb4f2 | |||
| 918b0e3770 | |||
| 88aae1706a | |||
| 3d2a8a3ca2 | |||
| a24fff5d5b | |||
| 4abddf5e6a | |||
| 1b4df9e79d | |||
| 45fe334cd7 | |||
| dd32a33d59 | |||
| 74b651a3a2 | |||
| 21fe7d76d3 |
112 changed files with 2872 additions and 896 deletions
|
|
@ -42,10 +42,13 @@ steps:
|
|||
- develop
|
||||
- stable
|
||||
image: node:20
|
||||
secrets:
|
||||
- SCW_ACCESS_KEY
|
||||
- SCW_SECRET_KEY
|
||||
- SCW_DEFAULT_ORGANIZATION_ID
|
||||
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
|
||||
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
|
||||
|
|
@ -64,11 +67,13 @@ 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
|
||||
|
|
|
|||
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -3,12 +3,30 @@ 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
|
||||
## Unreleased (3.19)
|
||||
|
||||
## 2026.03 (3.18.0) - 2026-03-14
|
||||
### REMOVED
|
||||
- dropped obsolete and buggy dm timeline
|
||||
|
||||
### Added
|
||||
- UI for conversations API, replacing the DM timeline.
|
||||
Here each thread (conversation) has it’s 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 - 2022-09-10
|
||||
## 2022.09 (3.2.0) - 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.
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ Make sure you have [Node.js](https://nodejs.org/) installed. You can check `/.wo
|
|||
|
||||
``` bash
|
||||
# install dependencies
|
||||
corepack enable
|
||||
npm install -g corepack
|
||||
yarn
|
||||
|
||||
# serve with hot reload at localhost:8080
|
||||
|
|
@ -55,3 +55,4 @@ Edit config.json for configuration.
|
|||
### Login methods
|
||||
|
||||
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.
|
||||
|
||||
|
|
|
|||
|
|
@ -15,12 +15,13 @@ 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 at `/static/stickers/myPack`". You can add as many packs as you like in this manner.
|
||||
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!
|
||||
|
||||
## Creating the pack
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
<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-->
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "pleroma_fe",
|
||||
"version": "3.10.0",
|
||||
"version": "3.18.0",
|
||||
"description": "A frontend for Akkoma instances",
|
||||
"author": "Roger Braun <roger@rogerbraun.net>",
|
||||
"private": true,
|
||||
|
|
@ -128,5 +128,6 @@
|
|||
"engines": {
|
||||
"node": ">= 16.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,8 @@ export default {
|
|||
{
|
||||
'-reverse': this.reverseLayout,
|
||||
'-no-sticky-headers': this.noSticky,
|
||||
'-has-new-post-button': this.newPostButtonShown
|
||||
'-has-new-post-button': this.newPostButtonShown,
|
||||
'-wide-timeline': this.widenTimeline
|
||||
},
|
||||
'-' + this.layoutType
|
||||
]
|
||||
|
|
@ -93,6 +94,9 @@ 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 },
|
||||
|
|
|
|||
|
|
@ -172,6 +172,10 @@ 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 {
|
||||
|
|
@ -187,12 +191,17 @@ 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);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ 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'
|
||||
|
|
@ -183,6 +182,12 @@ 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'])
|
||||
}
|
||||
|
||||
|
|
@ -247,17 +252,6 @@ 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 })
|
||||
|
|
@ -345,7 +339,7 @@ const setConfig = async ({ store }) => {
|
|||
const apiConfig = configInfos[0]
|
||||
const staticConfig = configInfos[1]
|
||||
|
||||
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
|
||||
await setSettings({ store, apiConfig, staticConfig })
|
||||
}
|
||||
|
||||
const checkOAuthToken = async ({ store }) => {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
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'
|
||||
|
|
@ -47,6 +49,9 @@ 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 } },
|
||||
|
|
@ -62,7 +67,6 @@ 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 },
|
||||
|
|
|
|||
|
|
@ -19,6 +19,17 @@
|
|||
height: 200px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
align-content: center;
|
||||
|
||||
.status-popover & {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
&.-nsfw-placeholder {
|
||||
.attachment-wrapper {
|
||||
align-content: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.description-container {
|
||||
|
|
@ -115,6 +126,24 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,6 +414,14 @@ 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]
|
||||
|
|
|
|||
|
|
@ -278,5 +278,7 @@
|
|||
&.-expanded.status-fadein {
|
||||
margin: calc(var(--status-margin, $status-margin) / 2);
|
||||
}
|
||||
|
||||
scroll-margin-block-start: var(--timeline-scroll-margin-top);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -105,6 +105,18 @@
|
|||
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"
|
||||
|
|
|
|||
80
src/components/dm_conv_card/dm_conv_card.js
Normal file
80
src/components/dm_conv_card/dm_conv_card.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
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
|
||||
134
src/components/dm_conv_card/dm_conv_card.vue
Normal file
134
src/components/dm_conv_card/dm_conv_card.vue
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<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>
|
||||
|
||||
</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>
|
||||
33
src/components/dm_conv_list/dm_conv_list.js
Normal file
33
src/components/dm_conv_list/dm_conv_list.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
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
|
||||
47
src/components/dm_conv_list/dm_conv_list.vue
Normal file
47
src/components/dm_conv_list/dm_conv_list.vue
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<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>
|
||||
57
src/components/dm_conv_recipients/dm_conv_recipients.js
Normal file
57
src/components/dm_conv_recipients/dm_conv_recipients.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
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
|
||||
82
src/components/dm_conv_recipients/dm_conv_recipients.vue
Normal file
82
src/components/dm_conv_recipients/dm_conv_recipients.vue
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<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>
|
||||
34
src/components/dm_conv_timeline/dm_conv_timeline.js
Normal file
34
src/components/dm_conv_timeline/dm_conv_timeline.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
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
|
||||
24
src/components/dm_conv_timeline/dm_conv_timeline.vue
Normal file
24
src/components/dm_conv_timeline/dm_conv_timeline.vue
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<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>
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
|
||||
const DMs = {
|
||||
computed: {
|
||||
timeline () {
|
||||
return this.$store.state.statuses.timelines.dms
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Timeline
|
||||
}
|
||||
}
|
||||
|
||||
export default DMs
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<template>
|
||||
<Timeline
|
||||
:title="$t('nav.dms')"
|
||||
:timeline="timeline"
|
||||
:timeline-name="'dms'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script src="./dm_timeline.js"></script>
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import StillImage from '../still-image/still-image.vue'
|
||||
|
||||
const EMOJI_SIZE = 32 + 8
|
||||
const GROUP_TITLE_HEIGHT = 24
|
||||
const BUFFER_SIZE = 3 * EMOJI_SIZE
|
||||
|
|
@ -17,6 +19,9 @@ const EmojiGrid = {
|
|||
resizeObserver: null
|
||||
}
|
||||
},
|
||||
components: {
|
||||
StillImage
|
||||
},
|
||||
mounted () {
|
||||
const rect = this.$refs.container.getBoundingClientRect()
|
||||
this.containerWidth = rect.width
|
||||
|
|
|
|||
|
|
@ -34,10 +34,11 @@
|
|||
@click.stop.prevent="onEmoji(item.emoji)"
|
||||
>
|
||||
<span v-if="!item.emoji.imageUrl">{{ item.emoji.replacement }}</span>
|
||||
<img
|
||||
<StillImage
|
||||
v-else
|
||||
:src="item.emoji.imageUrl"
|
||||
>
|
||||
no-stop-gifs="true"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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'
|
||||
|
||||
|
|
@ -120,7 +121,8 @@ const EmojiInput = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
EmojiPicker
|
||||
EmojiPicker,
|
||||
StillImage
|
||||
},
|
||||
computed: {
|
||||
padEmoji () {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
ref="picker"
|
||||
show-keep-open
|
||||
:class="{ hide: !showPicker }"
|
||||
:visible="showPicker"
|
||||
:enable-sticker-picker="enableStickerPicker"
|
||||
class="emoji-picker-panel"
|
||||
@emoji="insert"
|
||||
|
|
@ -47,10 +48,11 @@
|
|||
v-if="!suggestion.mfm"
|
||||
class="image"
|
||||
>
|
||||
<img
|
||||
<StillImage
|
||||
v-if="suggestion.img"
|
||||
:src="suggestion.img"
|
||||
>
|
||||
no-stop-gifs="true"
|
||||
/>
|
||||
<span v-else>{{ suggestion.replacement }}</span>
|
||||
</span>
|
||||
<div class="label">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const MFM_TAGS = ['blur', 'bounce', 'flip', 'font', 'jelly', 'jump', 'rainbow', 'rotate', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
|
||||
const MFM_TAGS = ['bg', 'blur', 'bounce', 'center', 'fg', 'flip', 'font', 'jelly', 'jump', 'position', 'rainbow', 'rotate', 'scale', 'shake', 'sparkle', 'spin', 'tada', 'twitch', 'x2', 'x3', 'x4']
|
||||
.map(tag => ({ displayText: tag, detailText: '$[' + tag + ' ]', replacement: '$[' + tag + ' ]', mfm: true }))
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
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,
|
||||
|
|
@ -26,12 +27,17 @@ const EmojiPicker = {
|
|||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
visible: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
keyword: '',
|
||||
activeGroup: 'standard',
|
||||
activeGroup: this.getDefaultGroup(),
|
||||
showingStickers: false,
|
||||
keepOpen: false
|
||||
}
|
||||
|
|
@ -39,7 +45,8 @@ const EmojiPicker = {
|
|||
components: {
|
||||
StickerPicker: defineAsyncComponent(() => import('../sticker_picker/sticker_picker.vue')),
|
||||
Checkbox,
|
||||
EmojiGrid
|
||||
EmojiGrid,
|
||||
StillImage
|
||||
},
|
||||
methods: {
|
||||
debouncedSearch: debounce(function (e) {
|
||||
|
|
@ -82,6 +89,11 @@ 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: {
|
||||
|
|
@ -148,6 +160,13 @@ 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,10 +18,11 @@
|
|||
@click.prevent="highlight(group.id)"
|
||||
>
|
||||
<span v-if="!group.first.imageUrl">{{ group.first.replacement }}</span>
|
||||
<img
|
||||
<StillImage
|
||||
v-else
|
||||
:src="group.first.imageUrl"
|
||||
>
|
||||
no-stop-gifs="true"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
v-if="stickerPickerEnabled"
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
@click="emojiOnClick(reaction.name, $event)"
|
||||
@mouseenter="fetchEmojiReactionsByIfMissing()"
|
||||
>
|
||||
<span
|
||||
<template
|
||||
v-if="reaction.url !== null"
|
||||
>
|
||||
<StillImage
|
||||
|
|
@ -19,16 +19,15 @@
|
|||
:title="reaction.name"
|
||||
:alt="reaction.name"
|
||||
class="reaction-emoji"
|
||||
height="2.55em"
|
||||
/>
|
||||
{{ reaction.count }}
|
||||
</span>
|
||||
<span v-else>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="reaction-emoji unicode-emoji">
|
||||
{{ reaction.name }}
|
||||
</span>
|
||||
<span>{{ reaction.count }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</button>
|
||||
</UserListPopover>
|
||||
<a
|
||||
|
|
@ -53,23 +52,26 @@
|
|||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.unicode-emoji {
|
||||
font-size: 210%;
|
||||
}
|
||||
|
||||
.emoji-reaction {
|
||||
padding: 0 0.5em;
|
||||
padding: 2px 0.5em;
|
||||
margin-right: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
align-items: end;
|
||||
|
||||
.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;
|
||||
|
|
@ -97,9 +99,9 @@
|
|||
}
|
||||
|
||||
.button-default.picked-reaction {
|
||||
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);
|
||||
&, &:hover {
|
||||
box-shadow: inset 0 0 0 1px var(--accent, $fallback--link);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ const ExtraButtons = {
|
|||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
copyLink () {
|
||||
navigator.clipboard.writeText(this.statusLink)
|
||||
navigator.clipboard.writeText(this.status.canonical_id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
|
|
@ -187,13 +187,6 @@ 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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -88,10 +88,8 @@ const Gallery = {
|
|||
set(this.sizes, id, { width, height })
|
||||
},
|
||||
rowStyle (row) {
|
||||
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))}%` }
|
||||
if (!row.audio && !row.minimal && !row.grid) {
|
||||
return { 'aspect-ratio': `1/${(1 / (row.items.length + 0.6))}` }
|
||||
}
|
||||
},
|
||||
itemStyle (id, row) {
|
||||
|
|
|
|||
|
|
@ -96,9 +96,15 @@
|
|||
|
||||
.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;
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const LoginForm = {
|
|||
}
|
||||
|
||||
oauthApi.getOrCreateApp(data)
|
||||
.then((app) => { oauthApi.login({ ...app, ...data }) })
|
||||
.then((app) => { oauthApi.login({ ...data, ...app }) })
|
||||
},
|
||||
submitPassword () {
|
||||
const { clientId } = this.oauth
|
||||
|
|
|
|||
|
|
@ -24,14 +24,15 @@
|
|||
:min-scale="pinchZoomMinScale"
|
||||
:reset-to-min-scale-limit="pinchZoomScaleResetLimit"
|
||||
>
|
||||
<img
|
||||
<StillImage
|
||||
:class="{ loading }"
|
||||
class="modal-image"
|
||||
:src="currentMedia.url"
|
||||
:alt="currentMedia.description"
|
||||
:title="currentMedia.description"
|
||||
@load="onImageLoaded"
|
||||
>
|
||||
:image-load-handler="onImageLoaded"
|
||||
no-stop-gifs="true"
|
||||
/>
|
||||
</PinchZoom>
|
||||
</SwipeClick>
|
||||
<VideoAttachment
|
||||
|
|
|
|||
|
|
@ -42,8 +42,14 @@ const mediaUpload = {
|
|||
.then((fileData) => {
|
||||
self.$emit('uploaded', fileData)
|
||||
self.decreaseUploadCount()
|
||||
}, (error) => {
|
||||
self.$emit('upload-failed', 'default')
|
||||
}, (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}`)
|
||||
self.decreaseUploadCount()
|
||||
})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
<input
|
||||
id="code"
|
||||
v-model="code"
|
||||
autocomplete="one-time-code"
|
||||
class="form-control"
|
||||
>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,24 @@
|
|||
<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"
|
||||
|
|
|
|||
|
|
@ -50,6 +50,13 @@ 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
|
||||
|
|
@ -70,10 +77,11 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
percentageForOption (count) {
|
||||
return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100)
|
||||
const total = this.totalFractionBase
|
||||
return total === 0 ? 0 : Math.round(count / total * 100)
|
||||
},
|
||||
resultTitle (option) {
|
||||
return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}`
|
||||
return `${option.votes_count}/${this.totalFractionBase} ${this.$t('polls.votes')}`
|
||||
},
|
||||
fetchPoll () {
|
||||
this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id })
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
<button
|
||||
v-if="options.length > 2"
|
||||
class="delete-option button-unstyled -hover-highlight"
|
||||
type="button"
|
||||
@click="deleteOption(index)"
|
||||
>
|
||||
<FAIcon icon="times" />
|
||||
|
|
@ -32,6 +33,7 @@
|
|||
<button
|
||||
v-if="options.length < maxOptions"
|
||||
class="add-option faint button-unstyled -hover-highlight"
|
||||
type="button"
|
||||
@click="addOption"
|
||||
>
|
||||
<FAIcon
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ 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'
|
||||
|
|
@ -85,6 +86,7 @@ const PostStatusForm = {
|
|||
'quoteId',
|
||||
'repliedUser',
|
||||
'attentions',
|
||||
'copyMessageLanguage',
|
||||
'copyMessageScope',
|
||||
'subject',
|
||||
'disableSubject',
|
||||
|
|
@ -148,13 +150,12 @@ const PostStatusForm = {
|
|||
const preset = this.$route.query.message
|
||||
let statusText = preset || ''
|
||||
|
||||
if (this.replyTo || this.quoteId) {
|
||||
if (this.replyTo || this.quoteId || this.repliedUser) {
|
||||
const currentUser = this.$store.state.users.currentUser
|
||||
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
||||
}
|
||||
|
||||
const { postContentType: contentType, postLanguage: defaultPostLanguage, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage, alwaysShowSubjectInput } = this.$store.getters.mergedConfig
|
||||
const postLanguage = defaultPostLanguage || interfaceToISOLanguage(interfaceLanguage)
|
||||
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, alwaysShowSubjectInput } = this.$store.getters.mergedConfig
|
||||
|
||||
let statusParams = {
|
||||
spoilerText: this.subject || '',
|
||||
|
|
@ -165,7 +166,7 @@ const PostStatusForm = {
|
|||
poll: {},
|
||||
mediaDescriptions: {},
|
||||
visibility: this.suggestedVisibility(),
|
||||
language: postLanguage,
|
||||
language: this.suggestedLanguage(),
|
||||
contentType
|
||||
}
|
||||
|
||||
|
|
@ -180,7 +181,7 @@ const PostStatusForm = {
|
|||
poll: this.statusPoll || {},
|
||||
mediaDescriptions: this.statusMediaDescriptions || {},
|
||||
visibility: this.statusScope || this.suggestedVisibility(),
|
||||
language: this.statusLanguage || postLanguage,
|
||||
language: this.statusLanguage || this.suggestedLanguage(),
|
||||
contentType: statusContentType
|
||||
}
|
||||
}
|
||||
|
|
@ -329,6 +330,7 @@ const PostStatusForm = {
|
|||
watch: {
|
||||
'newStatus': {
|
||||
deep: true,
|
||||
flush: 'sync',
|
||||
handler () {
|
||||
this.statusChanged()
|
||||
}
|
||||
|
|
@ -341,17 +343,22 @@ const PostStatusForm = {
|
|||
this.saveDraft()
|
||||
},
|
||||
clearStatus () {
|
||||
const newStatus = this.newStatus
|
||||
const config = this.$store.getters.mergedConfig
|
||||
this.newStatus = {
|
||||
status: '',
|
||||
spoilerText: '',
|
||||
files: [],
|
||||
visibility: newStatus.visibility,
|
||||
contentType: newStatus.contentType,
|
||||
language: newStatus.language,
|
||||
nsfw: !!config.sensitiveByDefault,
|
||||
visibility: this.suggestedVisibility(),
|
||||
contentType: config.postContentType,
|
||||
language: this.suggestedLanguage(),
|
||||
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()
|
||||
|
|
@ -511,7 +518,7 @@ const PostStatusForm = {
|
|||
addMediaFile (fileInfo) {
|
||||
this.newStatus.files.push(fileInfo)
|
||||
|
||||
if (this.$store.getters.mergedConfig.sensitiveIfSubject && this.newStatus.spoilerText !== '') {
|
||||
if (this.$store.getters.mergedConfig.sensitiveIfSubject && this.newStatus.spoilerText !== '' || !!this.$store.getters.mergedConfig.sensitiveByDefault) {
|
||||
this.newStatus.nsfw = true
|
||||
}
|
||||
this.$emit('resize', { delayed: true })
|
||||
|
|
@ -760,16 +767,19 @@ const PostStatusForm = {
|
|||
openProfileTab () {
|
||||
this.$store.dispatch('openSettingsModalTab', 'profile')
|
||||
},
|
||||
suggestedVisibility () {
|
||||
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
|
||||
}
|
||||
suggestedLanguage () {
|
||||
// Make sure the inherited language is actually valid
|
||||
if (this.postLanguageOptions.find(o => o.value === this.copyMessageLanguage)) {
|
||||
return this.copyMessageLanguage
|
||||
}
|
||||
return this.$store.state.users.currentUser.default_scope
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
>
|
||||
<button
|
||||
class="button-unstyled -link"
|
||||
type="button"
|
||||
@click="openProfileTab"
|
||||
>
|
||||
{{ $t('post_status.account_not_locked_warning_link') }}
|
||||
|
|
@ -136,6 +137,7 @@
|
|||
class="form-post-subject"
|
||||
@input="onSubjectInput"
|
||||
@focus="focusSubjectInput()"
|
||||
@keydown.exact.enter.prevent
|
||||
>
|
||||
</EmojiInput>
|
||||
<i18n-t
|
||||
|
|
@ -194,6 +196,7 @@
|
|||
>
|
||||
<scope-selector
|
||||
v-if="!disableVisibilitySelector"
|
||||
ref="scopeselector"
|
||||
:user-default="userDefaultScope"
|
||||
:original-scope="copyMessageScope"
|
||||
:initial-scope="newStatus.visibility"
|
||||
|
|
@ -201,10 +204,11 @@
|
|||
/>
|
||||
|
||||
<div
|
||||
class="format-selector-container">
|
||||
class="format-selector-container"
|
||||
>
|
||||
<div
|
||||
class="format-selector"
|
||||
>
|
||||
>
|
||||
<Select
|
||||
id="post-language"
|
||||
v-model="newStatus.language"
|
||||
|
|
@ -272,6 +276,7 @@
|
|||
<button
|
||||
class="emoji-icon button-unstyled"
|
||||
:title="$t('emoji.add_emoji')"
|
||||
type="button"
|
||||
@click="showEmojiPicker"
|
||||
>
|
||||
<FAIcon icon="smile-beam" />
|
||||
|
|
@ -281,6 +286,7 @@
|
|||
class="poll-icon button-unstyled"
|
||||
:class="{ selected: pollFormVisible }"
|
||||
:title="$t('polls.add_poll')"
|
||||
type="button"
|
||||
@click="togglePollForm"
|
||||
>
|
||||
<FAIcon icon="poll-h" />
|
||||
|
|
@ -290,6 +296,7 @@
|
|||
class="spoiler-icon button-unstyled"
|
||||
:class="{ selected: subjectVisible }"
|
||||
:title="$t('post_status.toggle_content_warning')"
|
||||
type="button"
|
||||
@click="toggleSubjectVisible"
|
||||
>
|
||||
<FAIcon icon="eye-slash" />
|
||||
|
|
|
|||
|
|
@ -7,8 +7,16 @@ const QuoteButton = {
|
|||
name: 'QuoteButton',
|
||||
props: ['status', 'quoting', 'visibility'],
|
||||
computed: {
|
||||
loggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="(visibility === 'public' || visibility === 'unlisted') && loggedIn"
|
||||
v-if="showButton"
|
||||
class="QuoteButton"
|
||||
>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
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'
|
||||
|
||||
|
|
@ -7,12 +9,16 @@ library.add(faRetweet)
|
|||
const RetweetButton = {
|
||||
props: ['status', 'loggedIn', 'visibility'],
|
||||
components: {
|
||||
ConfirmModal
|
||||
ConfirmModal,
|
||||
ScopeSelector
|
||||
},
|
||||
data () {
|
||||
const maxScope = this.status.visibility
|
||||
const defaultScope = this.$store.state.users.currentUser.default_scope
|
||||
return {
|
||||
animated: false,
|
||||
showingConfirmDialog: false
|
||||
showingConfirmDialog: false,
|
||||
retweetVisibility: scopeUtils.negotiate(defaultScope, maxScope)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -25,7 +31,7 @@ const RetweetButton = {
|
|||
},
|
||||
doRetweet () {
|
||||
if (!this.status.repeated) {
|
||||
this.$store.dispatch('retweet', { id: this.status.id })
|
||||
this.$store.dispatch('retweet', { id: this.status.id, visibility: this.retweetVisibility })
|
||||
} else {
|
||||
this.$store.dispatch('unretweet', { id: this.status.id })
|
||||
}
|
||||
|
|
@ -40,6 +46,9 @@ const RetweetButton = {
|
|||
},
|
||||
hideConfirmDialog () {
|
||||
this.showingConfirmDialog = false
|
||||
},
|
||||
changeVis (visibility) {
|
||||
this.retweetVisibility = visibility
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -54,7 +63,13 @@ 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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,12 @@
|
|||
@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>
|
||||
|
|
|
|||
|
|
@ -121,6 +121,19 @@ 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
|
||||
|
|
@ -191,6 +204,15 @@ 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) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import {
|
|||
faGlobe
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
import scopeUtils from 'src/lib/scope_utils.js'
|
||||
|
||||
library.add(
|
||||
faEnvelope,
|
||||
faGlobe,
|
||||
|
|
@ -13,18 +15,11 @@ 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'
|
||||
|
|
@ -39,16 +34,16 @@ const ScopeSelector = {
|
|||
return !this.showPublic && !this.showUnlisted && !this.showPrivate && !this.showDirect
|
||||
},
|
||||
showPublic () {
|
||||
return this.originalScope !== 'direct' && this.shouldShow('public')
|
||||
return this.shouldShow('public')
|
||||
},
|
||||
showLocal () {
|
||||
return this.originalScope !== 'direct' && this.shouldShow('local')
|
||||
return this.shouldShow('local')
|
||||
},
|
||||
showUnlisted () {
|
||||
return this.originalScope !== 'direct' && this.shouldShow('unlisted')
|
||||
return this.shouldShow('unlisted')
|
||||
},
|
||||
showPrivate () {
|
||||
return this.originalScope !== 'direct' && this.shouldShow('private')
|
||||
return this.shouldShow('private')
|
||||
},
|
||||
showDirect () {
|
||||
return this.shouldShow('direct')
|
||||
|
|
@ -65,15 +60,10 @@ const ScopeSelector = {
|
|||
},
|
||||
methods: {
|
||||
shouldShow (scope) {
|
||||
if (!this.originalScope) {
|
||||
if (!this.originalScope)
|
||||
return true
|
||||
}
|
||||
|
||||
if (this.originalScope === 'local') {
|
||||
return scope === 'direct' || scope === 'local'
|
||||
}
|
||||
|
||||
return SCOPE_LEVELS[scope] <= SCOPE_LEVELS[this.originalScope]
|
||||
else
|
||||
return scopeUtils.isSubScope(this.originalScope, scope)
|
||||
},
|
||||
changeVis (scope) {
|
||||
this.currentScope = scope
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const SettingsModal = {
|
|||
this.$store.dispatch('closeSettingsModal')
|
||||
},
|
||||
logout () {
|
||||
this.$router.replace('/main/public')
|
||||
this.$router.replace(this.$store.state.instance.redirectRootNoLogin || '/main/all')
|
||||
this.$store.dispatch('closeSettingsModal')
|
||||
this.$store.dispatch('logout')
|
||||
},
|
||||
|
|
|
|||
|
|
@ -159,6 +159,16 @@
|
|||
{{ $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') }}
|
||||
|
|
@ -269,6 +279,11 @@
|
|||
{{ $t('settings.right_sidebar') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="widenTimeline">
|
||||
{{ $t('settings.widen_timeline') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
v-if="user"
|
||||
|
|
@ -320,6 +335,11 @@
|
|||
{{ $t('settings.confirm_dialogs_deny_follow') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="modalOnDeleteDMConversation">
|
||||
{{ $t('settings.confirm_dialogs_delete_dm_conv') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,25 @@
|
|||
import { extractCommit } from 'src/services/version/version.service'
|
||||
|
||||
const pleromaFeCommitUrl = 'https://akkoma.dev/AkkomaGang/pleroma-fe/commit/'
|
||||
const pleromaBeCommitUrl = 'https://akkoma.dev/AkkomaGang/akkoma/commit/'
|
||||
function joinURL(base, subpath) {
|
||||
return URL.parse(subpath, base)?.href || "invalid base URL"
|
||||
}
|
||||
|
||||
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 pleromaFeCommitUrl + this.frontendVersion
|
||||
return joinURL(this.frontendCommitUrl, this.frontendVersion)
|
||||
},
|
||||
backendVersionLink () {
|
||||
return pleromaBeCommitUrl + extractCommit(this.backendVersion)
|
||||
return joinURL(this.backendCommitUrl, extractCommit(this.backendVersion))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
faHome,
|
||||
faComments,
|
||||
faBolt,
|
||||
faBookmark,
|
||||
faUserPlus,
|
||||
faBullhorn,
|
||||
faSearch,
|
||||
|
|
@ -25,6 +26,7 @@ library.add(
|
|||
faHome,
|
||||
faComments,
|
||||
faBolt,
|
||||
faBookmark,
|
||||
faUserPlus,
|
||||
faBullhorn,
|
||||
faSearch,
|
||||
|
|
@ -43,10 +45,6 @@ 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: {
|
||||
|
|
@ -59,6 +57,9 @@ 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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -55,6 +55,24 @@
|
|||
/> {{ $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"
|
||||
|
|
@ -67,6 +85,18 @@
|
|||
/> {{ $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">
|
||||
|
|
|
|||
|
|
@ -169,8 +169,8 @@ const Status = {
|
|||
},
|
||||
computed: {
|
||||
...controlledOrUncontrolledGetters(['replying', 'quoting', 'mediaPlaying']),
|
||||
muteWords () {
|
||||
return this.mergedConfig.muteWords
|
||||
muteWordRules () {
|
||||
return this.$store.getters.parsedConfigVal('muteWords')
|
||||
},
|
||||
showReasonMutedThread () {
|
||||
return (
|
||||
|
|
@ -215,6 +215,7 @@ 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
|
||||
|
|
@ -230,7 +231,7 @@ const Status = {
|
|||
return !!this.currentUser
|
||||
},
|
||||
muteWordHits () {
|
||||
return muteWordHits(this.status, this.muteWords)
|
||||
return muteWordHits(this.status, this.muteWordRules)
|
||||
},
|
||||
rtBotStatus () {
|
||||
return this.statusoid.user.bot
|
||||
|
|
@ -440,6 +441,9 @@ 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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -99,20 +99,43 @@
|
|||
<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>
|
||||
|
||||
|
|
@ -519,6 +542,7 @@
|
|||
: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"
|
||||
|
|
@ -533,6 +557,7 @@
|
|||
: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"
|
||||
|
|
|
|||
|
|
@ -41,7 +41,8 @@ const StatusContent = {
|
|||
postLength: this.status.text.length,
|
||||
parseReadyDone: false,
|
||||
renderMisskeyMarkdown,
|
||||
translateFrom: null
|
||||
translateFrom: null,
|
||||
translating: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -135,7 +136,10 @@ const StatusContent = {
|
|||
},
|
||||
translateStatus () {
|
||||
const translateTo = this.$store.getters.mergedConfig.translationLanguage || this.$store.state.instance.interfaceLanguage
|
||||
this.$store.dispatch('translateStatus', { id: this.status.id, language: translateTo, from: this.translateFrom })
|
||||
this.translating = true
|
||||
this.$store.dispatch(
|
||||
'translateStatus', { id: this.status.id, language: translateTo, from: this.translateFrom }
|
||||
).finally(() => { this.translating = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
.StatusBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.translation {
|
||||
border: 1px solid var(--accent, $fallback--link);
|
||||
|
|
@ -23,24 +24,6 @@
|
|||
transition: 0.05s;
|
||||
}
|
||||
|
||||
._mfm_x2_ {
|
||||
.emoji {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
._mfm_x3_ {
|
||||
.emoji {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
._mfm_x4_ {
|
||||
.emoji {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.attachments {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
class="StatusBody"
|
||||
:class="{ '-compact': compact, 'mfm-disabled': !renderMisskeyMarkdown }"
|
||||
:class="{ '-compact': compact }"
|
||||
>
|
||||
<div class="body">
|
||||
<div
|
||||
|
|
@ -91,6 +91,7 @@
|
|||
{{ ' ' }}
|
||||
<button
|
||||
class="btn button-default"
|
||||
:disabled="translating"
|
||||
@click="translateStatus"
|
||||
>
|
||||
{{ $t('status.translate') }}
|
||||
|
|
|
|||
423
src/components/status_content/mfm.scss
Normal file
423
src/components/status_content/mfm.scss
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
/**
|
||||
* "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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -106,6 +106,9 @@ const StatusContent = {
|
|||
renderMisskeyMarkdown () {
|
||||
return this.mergedConfig.renderMisskeyMarkdown
|
||||
},
|
||||
hasResolvedQuote () {
|
||||
return !!this.status.quote
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
class="StatusContent"
|
||||
:class="{ '-compact': compact, 'mfm-hover': renderMfmOnHover, 'mfm-disabled': !renderMisskeyMarkdown }"
|
||||
:class="{ '-compact': compact, 'mfm-hover': renderMfmOnHover, 'mfm-disabled': !renderMisskeyMarkdown, 'quote-resolved': hasResolvedQuote }"
|
||||
>
|
||||
<slot name="header" />
|
||||
<StatusBody
|
||||
|
|
@ -64,6 +64,7 @@
|
|||
</template>
|
||||
|
||||
<script src="./status_content.js"></script>
|
||||
<style lang="scss" src="./mfm.scss" />
|
||||
<style lang="scss">
|
||||
.StatusContent {
|
||||
flex: 1;
|
||||
|
|
@ -75,28 +76,15 @@
|
|||
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-inline,
|
||||
.quote-resolved .quote-inline,
|
||||
.quote + .link-preview {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.quote-resolved .quote .quote-inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -7,12 +7,19 @@ const StillImage = {
|
|||
'imageLoadHandler',
|
||||
'alt',
|
||||
'height',
|
||||
'width'
|
||||
'width',
|
||||
'noStopGifs'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
stopGifs: this.$store.getters.mergedConfig.stopGifs || window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
||||
stopGifs:
|
||||
!this.noStopGifs
|
||||
&& (
|
||||
this.$store.getters.mergedConfig.stopGifs
|
||||
|| window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
),
|
||||
isAnimated: false,
|
||||
isPixelArt: false,
|
||||
imageTypeLabel: ''
|
||||
}
|
||||
},
|
||||
|
|
@ -34,11 +41,18 @@ 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
|
||||
|
||||
|
|
@ -59,39 +73,51 @@ const StillImage = {
|
|||
},
|
||||
detectAnimationWithFetch (image) {
|
||||
// Browser Cache should ensure image doesn't get loaded twice if cache exists
|
||||
fetch(image.src, {
|
||||
return fetch(image.src, {
|
||||
referrerPolicy: 'same-origin'
|
||||
})
|
||||
.then(data => {
|
||||
// We don't need to read the whole file so only call it once
|
||||
data.body.getReader().read()
|
||||
return data.body.getReader().read()
|
||||
.then(reader => {
|
||||
// Ordered from least to most intensive
|
||||
if (this.isGIF(reader.value)) {
|
||||
this.isAnimated = true
|
||||
this.setLabel('GIF')
|
||||
return
|
||||
return true
|
||||
}
|
||||
if (this.isAnimatedWEBP(reader.value)) {
|
||||
this.isAnimated = true
|
||||
this.setLabel('WEBP')
|
||||
return
|
||||
return true
|
||||
}
|
||||
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)
|
||||
},
|
||||
detectWithoutMediaProxy (image) {
|
||||
// We'll just assume that gifs and webp are animated
|
||||
async detectWithoutMediaProxy (image) {
|
||||
// If media is local, we can still fetch,
|
||||
// otherwise CORS won’t allow it and we fall back to checking extensions
|
||||
// (XXX: ideally we’d _only_ fetch if it’s 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
|
||||
const extension = image.src.split('.').pop().toLowerCase()
|
||||
|
||||
if (extension === 'gif') {
|
||||
|
|
@ -104,18 +130,15 @@ const StillImage = {
|
|||
this.setLabel('WEBP')
|
||||
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)
|
||||
// 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
|
||||
}
|
||||
|
||||
// Hail mary for extensionless files we cannot fetch
|
||||
},
|
||||
setLabel (name) {
|
||||
this.imageTypeLabel = name;
|
||||
|
|
@ -207,7 +230,7 @@ const StillImage = {
|
|||
}
|
||||
|
||||
context.clearRect(0, 0, canvas.width, canvas.height); // Clear the previous unscaled image
|
||||
context.imageSmoothingEnabled = true;
|
||||
context.imageSmoothingEnabled = !this.isPixelArt;
|
||||
context.imageSmoothingQuality = 'high';
|
||||
|
||||
// Draw the good one for realsies
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div
|
||||
ref="still-image"
|
||||
class="still-image"
|
||||
:class="{ animated: animated }"
|
||||
:class="{ animated: animated, pixelart: isPixelArt }"
|
||||
:style="style"
|
||||
>
|
||||
<div
|
||||
|
|
@ -95,5 +95,8 @@
|
|||
visibility: visible;
|
||||
}
|
||||
}
|
||||
&.pixelart {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const Timeline = {
|
|||
'title',
|
||||
'userId',
|
||||
'listId',
|
||||
'conversationId',
|
||||
'tag',
|
||||
'embedded',
|
||||
'count',
|
||||
|
|
@ -119,6 +120,7 @@ const Timeline = {
|
|||
showImmediately,
|
||||
userId: this.userId,
|
||||
listId: this.listId,
|
||||
conversationId: this.conversationId,
|
||||
tag: this.tag
|
||||
})
|
||||
},
|
||||
|
|
@ -181,6 +183,7 @@ const Timeline = {
|
|||
showImmediately: true,
|
||||
userId: this.userId,
|
||||
listId: this.listId,
|
||||
conversationId: this.conversationId,
|
||||
tag: this.tag
|
||||
}).then(({ statuses }) => {
|
||||
if (statuses && statuses.length === 0) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,13 @@
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,12 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="$slots.extraHeading"
|
||||
class="timeline-extra-heading"
|
||||
>
|
||||
<slot name="extraHeading" />
|
||||
</div>
|
||||
<div :class="classes.body">
|
||||
<div
|
||||
ref="timeline"
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export const timelineNames = () => {
|
|||
return {
|
||||
'friends': 'nav.home_timeline',
|
||||
'bookmarks': 'nav.bookmarks',
|
||||
'dms': 'nav.dms',
|
||||
'dm_conversation': 'nav.dm_conversation',
|
||||
'public-timeline': 'nav.public_tl',
|
||||
'public-external-timeline': 'nav.twkn',
|
||||
'bubble-timeline': 'nav.bubble_timeline'
|
||||
|
|
|
|||
|
|
@ -80,22 +80,6 @@
|
|||
>{{ $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>
|
||||
|
||||
|
|
|
|||
|
|
@ -80,22 +80,6 @@
|
|||
>{{ $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>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ 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'
|
||||
|
|
|
|||
|
|
@ -117,6 +117,11 @@ 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: {
|
||||
|
|
@ -192,7 +197,7 @@ export default {
|
|||
this.$store.dispatch('setCurrentMedia', attachment)
|
||||
},
|
||||
mentionUser () {
|
||||
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
|
||||
this.$store.dispatch('openPostStatusModal', { repliedUser: this.user })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,13 @@
|
|||
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;
|
||||
|
|
@ -53,7 +60,6 @@
|
|||
}
|
||||
|
||||
&-bio {
|
||||
text-align: center;
|
||||
display: block;
|
||||
line-height: 1.3;
|
||||
padding: 1em;
|
||||
|
|
@ -100,15 +106,14 @@
|
|||
padding: 0 26px;
|
||||
|
||||
.container {
|
||||
min-width: 0;
|
||||
padding: 16px 0 6px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
max-height: 56px;
|
||||
|
||||
> * {
|
||||
min-width: 0;
|
||||
}
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"pfp name edit"
|
||||
"pfp summary summary"
|
||||
"stats stats stats";
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: start;
|
||||
|
||||
.Avatar {
|
||||
--_avatarShadowBox: var(--avatarShadow);
|
||||
|
|
@ -123,6 +128,7 @@
|
|||
}
|
||||
|
||||
&-avatar-link {
|
||||
grid-area: pfp;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
|
|
@ -153,8 +159,8 @@
|
|||
|
||||
.external-link-button, .edit-profile-button {
|
||||
cursor: pointer;
|
||||
width: 2.5em;
|
||||
text-align: center;
|
||||
width: 2.3em;
|
||||
text-align: right;
|
||||
margin: -0.5em 0;
|
||||
padding: 0.5em 0;
|
||||
|
||||
|
|
@ -165,12 +171,16 @@
|
|||
}
|
||||
|
||||
.user-summary {
|
||||
display: block;
|
||||
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;
|
||||
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;
|
||||
|
|
@ -178,56 +188,82 @@
|
|||
|
||||
--emoji-size: 1.7em;
|
||||
|
||||
.top-line,
|
||||
.bottom-line {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.user-name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex: 1 1 auto;
|
||||
margin-right: 1em;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.bottom-line {
|
||||
font-weight: light;
|
||||
font-size: 1.1em;
|
||||
align-items: baseline;
|
||||
|
||||
.lock-icon {
|
||||
.user-locked {
|
||||
margin-left: 0.5em;
|
||||
grid-area: lock;
|
||||
}
|
||||
|
||||
.user-screen-name {
|
||||
min-width: 1px;
|
||||
flex: 0 1 auto;
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
color: $fallback--lightText;
|
||||
color: var(--lightText, $fallback--lightText);
|
||||
grid-area: hand;
|
||||
}
|
||||
|
||||
.dailyAvg {
|
||||
min-width: 1px;
|
||||
flex: 0 0 auto;
|
||||
margin-left: 1em;
|
||||
font-size: 0.7em;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
grid-area: avg;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
flex: none;
|
||||
color: $fallback--text;
|
||||
color: var(--alertNeutralText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--alertNeutral, $fallback--fg);
|
||||
.user-roles {
|
||||
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;
|
||||
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
|
||||
}
|
||||
|
||||
.user-meta {
|
||||
margin-bottom: .15em;
|
||||
display: flex;
|
||||
|
|
@ -290,34 +326,21 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@
|
|||
class="background-image"
|
||||
/>
|
||||
<div class="panel-heading -flexible-height">
|
||||
<div class="user-info">
|
||||
<div
|
||||
class="user-info"
|
||||
:class="{ '-compact': compactUserInfo }"
|
||||
>
|
||||
<div class="container">
|
||||
<a
|
||||
v-if="allowZoomingAvatar"
|
||||
|
|
@ -29,6 +32,7 @@
|
|||
</a>
|
||||
<router-link
|
||||
v-else
|
||||
class="user-info-avatar-link"
|
||||
:to="userProfileLink(user)"
|
||||
>
|
||||
<UserAvatar
|
||||
|
|
@ -36,94 +40,124 @@
|
|||
:user="user"
|
||||
/>
|
||||
</router-link>
|
||||
<RichContent
|
||||
:title="user.name"
|
||||
class="user-name"
|
||||
:html="user.name"
|
||||
:emoji="user.emoji"
|
||||
/>
|
||||
<div class="user-summary">
|
||||
<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"
|
||||
<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"
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
{{ $t('user_card.deactivated') }}
|
||||
</span>
|
||||
<span
|
||||
v-if="!mergedConfig.hideUserStats && !hideBio"
|
||||
class="dailyAvg"
|
||||
>{{ dailyAvg }} {{ $t('user_card.per_day') }}</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>
|
||||
</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">
|
||||
|
|
@ -269,38 +303,13 @@
|
|||
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">
|
||||
|
|
|
|||
|
|
@ -486,6 +486,7 @@
|
|||
"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",
|
||||
|
|
@ -496,6 +497,7 @@
|
|||
"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",
|
||||
|
|
@ -934,6 +936,7 @@
|
|||
"title": "Version"
|
||||
},
|
||||
"virtual_scrolling": "Anzeige der Zeitleiste optimieren",
|
||||
"widen_timeline": "Zeitleiste verbreitern, um horizontalen Platz zu füllen",
|
||||
"word_filter": "Wortfilter",
|
||||
"wordfilter": "Wortfilter"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,15 +7,21 @@
|
|||
"replace": "Αντικατάσταση"
|
||||
},
|
||||
"mrf_policies": "Ενεργοποιημένες πολιτικές MRF",
|
||||
"mrf_policies_desc": "",
|
||||
"mrf_policies_desc": "Οι πολιτικές MRF επηρεάζουν τη συμπεριφορά του instance. Οι ακόλουθες πολιτικές είναι ενεργοποιημένες:",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
|
|
@ -32,11 +38,20 @@
|
|||
"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": "Περισσότερα",
|
||||
|
|
@ -47,11 +62,15 @@
|
|||
"today": "Σήμερα"
|
||||
},
|
||||
"domain_mute_card": {
|
||||
"mute": "Σίγαση"
|
||||
"mute": "Σίγαση",
|
||||
"mute_progress": "Σίγαση…",
|
||||
"unmute": "Αφαίρεση σίγασης",
|
||||
"unmute_progress": "Αφαίρεση σίγασης…"
|
||||
},
|
||||
"emoji": {
|
||||
"add_emoji": "Εισαγωγή emoji",
|
||||
"load_all": "Φόρτωση όλων των {emojiAmount} emoji",
|
||||
"load_all_hint": "Φορτώθηκαν τα πρώτα {saneAmount} emoji, η φόρτωση όλων των emoji μπορεί να προκαλέσει θέματα απόδοσης.",
|
||||
"recent": "Χρησιμοποιήθηκαν πρόσφατα",
|
||||
"search_emoji": "Αναζήτηση για ένα emoji",
|
||||
"stickers": "Αυτοκόλλητα"
|
||||
|
|
@ -63,7 +82,10 @@
|
|||
"export": "Εξαγωγή"
|
||||
},
|
||||
"features_panel": {
|
||||
"text_limit": "Όριο κειμένου"
|
||||
"media_proxy": "Διαμεσολαβητής μέσων",
|
||||
"text_limit": "Όριο κειμένου",
|
||||
"title": "Δυνατότητες",
|
||||
"upload_limit": "Όριο upload"
|
||||
},
|
||||
"file_type": {
|
||||
"audio": "Ήχος",
|
||||
|
|
@ -79,6 +101,8 @@
|
|||
"enable": "Ενεργοποίηση",
|
||||
"error_retry": "Παρακαλώ δοκιμάστε ξανά",
|
||||
"flash_content": "Κάντε κλικ για την εμφάνιση Flash περιεχομένου με τη χρήση του Ruffle (Πειραματικό, μπορεί να μη λειτουργεί).",
|
||||
"flash_fail": "Η φόρτωση περιεχομένου flash απέτυχε, δείτε στην κονσόλα για λεπτομέρειες.",
|
||||
"generic_error": "Προέκυψε ένα σφάλμα",
|
||||
"loading": "Φόρτωση…",
|
||||
"more": "Περισσότερα",
|
||||
"optional": "προαιρετικό",
|
||||
|
|
@ -104,6 +128,7 @@
|
|||
"save_without_cropping": "Αποθήκευση χωρίς περικοπή"
|
||||
},
|
||||
"importer": {
|
||||
"error": "Προέκυψε ένα σφάλμα κατά την εισαγωγή αυτού του αρχείου.",
|
||||
"success": "Εισήχθη επιτυχώς."
|
||||
},
|
||||
"languages": {
|
||||
|
|
|
|||
|
|
@ -243,6 +243,27 @@
|
|||
"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",
|
||||
|
|
@ -306,6 +327,9 @@
|
|||
"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",
|
||||
|
|
@ -488,6 +512,7 @@
|
|||
"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)",
|
||||
|
|
@ -502,11 +527,13 @@
|
|||
"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",
|
||||
|
|
@ -726,7 +753,7 @@
|
|||
"security": "Security",
|
||||
"security_tab": "Security",
|
||||
"sensitive_by_default": "Mark posts as sensitive by default",
|
||||
"sensitive_if_subject": "Automatically mark images as sensitive if a content warning is specified",
|
||||
"sensitive_if_subject": "Automatically mark post 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",
|
||||
|
|
@ -948,6 +975,7 @@
|
|||
},
|
||||
"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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -407,7 +407,8 @@
|
|||
"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.",
|
||||
|
|
@ -491,6 +492,7 @@
|
|||
"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",
|
||||
|
|
@ -501,6 +503,7 @@
|
|||
"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",
|
||||
|
|
@ -536,6 +539,8 @@
|
|||
"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",
|
||||
|
|
@ -599,7 +604,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 le son (les « gifs » de Mastodon)",
|
||||
"loop_video_silent_only": "Boucle uniquement les vidéos sans 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",
|
||||
|
|
@ -677,7 +682,9 @@
|
|||
"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",
|
||||
|
|
@ -745,6 +752,7 @@
|
|||
"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",
|
||||
|
|
@ -920,8 +928,13 @@
|
|||
"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",
|
||||
|
|
@ -1044,6 +1057,7 @@
|
|||
"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é",
|
||||
|
|
@ -1053,6 +1067,7 @@
|
|||
"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": {
|
||||
|
|
@ -1117,6 +1132,7 @@
|
|||
"block_confirm_title": "Bloquer l'utilisateur",
|
||||
"block_progress": "Blocage…",
|
||||
"blocked": "Bloqué !",
|
||||
"blocks_you": "Vous bloque !",
|
||||
"bot": "Robot",
|
||||
"deactivated": "Désactivé",
|
||||
"deny": "Rejeter",
|
||||
|
|
@ -1131,7 +1147,10 @@
|
|||
"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 !",
|
||||
|
|
@ -1156,12 +1175,14 @@
|
|||
"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",
|
||||
|
|
@ -1171,11 +1192,13 @@
|
|||
"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"
|
||||
|
|
|
|||
363
src/i18n/it.json
363
src/i18n/it.json
|
|
@ -503,7 +503,13 @@
|
|||
"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",
|
||||
|
|
@ -511,11 +517,15 @@
|
|||
"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",
|
||||
|
|
@ -543,18 +553,23 @@
|
|||
"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",
|
||||
|
|
@ -567,10 +582,14 @@
|
|||
"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",
|
||||
|
|
@ -594,6 +613,12 @@
|
|||
},
|
||||
"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",
|
||||
|
|
@ -603,6 +628,7 @@
|
|||
"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",
|
||||
|
|
@ -620,6 +646,7 @@
|
|||
"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",
|
||||
|
|
@ -628,7 +655,9 @@
|
|||
"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",
|
||||
|
|
@ -642,6 +671,8 @@
|
|||
"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",
|
||||
|
|
@ -666,12 +697,15 @@
|
|||
"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": {
|
||||
|
|
@ -780,66 +814,80 @@
|
|||
},
|
||||
"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 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"
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"switcher": {
|
||||
"clear_all": "Azzera tutto",
|
||||
"clear_opacity": "Rimuovi opacità",
|
||||
"clear_opacity": "Azzera opacità",
|
||||
"help": {
|
||||
"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.",
|
||||
"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.",
|
||||
"snapshot_present": "Tutti i valori sono sostituiti dall'anteprima del tema. Puoi invece caricare i suoi contenuti.",
|
||||
"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."
|
||||
"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."
|
||||
},
|
||||
"keep_as_is": "Mantieni tal quale",
|
||||
"keep_as_is": "Mantieni com'è",
|
||||
"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": "Reimposta",
|
||||
"reset": "Azzera",
|
||||
"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 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_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_line_noop": "Non copiare",
|
||||
"text": "Testo",
|
||||
"theme": "Tema",
|
||||
"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.",
|
||||
"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",
|
||||
"token": "Token",
|
||||
"tooltipRadius": "Suggerimenti/avvisi",
|
||||
"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",
|
||||
"type_domains_to_mute": "Cerca domini da silenziare",
|
||||
"upload_a_photo": "Carica un'immagine",
|
||||
"upload_a_photo": "Carica una foto",
|
||||
"useStreamingApi": "Ricevi messaggi e notifiche in tempo reale",
|
||||
"useStreamingApiWarning": "(Sconsigliato, sperimentale, può saltare messaggi)",
|
||||
"useStreamingApiWarning": "",
|
||||
"use_blurhash": "Usa blurhash per anteprime NSFW",
|
||||
"use_contain_fit": "Non ritagliare le anteprime degli allegati",
|
||||
"use_one_click_nsfw": "Apri media offuscati con un solo click",
|
||||
"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",
|
||||
"user_mutes": "Utenti",
|
||||
"user_settings": "Impostazioni Utente",
|
||||
"user_profile_default_tab": "Scheda predefinita sul profilo degli utenti",
|
||||
"user_profiles": "Profili utente",
|
||||
"user_settings": "Impostazioni utente",
|
||||
"valid_until": "Valido fino a",
|
||||
"values": {
|
||||
"false": "no",
|
||||
|
|
@ -847,86 +895,141 @@
|
|||
},
|
||||
"version": {
|
||||
"backend_version": "Versione backend",
|
||||
"frontend_version": "Versione interfaccia",
|
||||
"frontend_version": "Versione frontend",
|
||||
"title": "Versione"
|
||||
},
|
||||
"virtual_scrolling": "Velocizza l'elaborazione delle sequenze",
|
||||
"word_filter": "Parole filtrate"
|
||||
"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}\"…"
|
||||
},
|
||||
"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",
|
||||
"copy_link": "Copia collegamento",
|
||||
"delete": "Elimina messaggio",
|
||||
"delete_confirm": "Vuoi veramente eliminare questo messaggio?",
|
||||
"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}",
|
||||
"expand": "Espandi",
|
||||
"external_source": "Vai all'origine",
|
||||
"external_source": "Fonte originale",
|
||||
"favorites": "Preferiti",
|
||||
"hide_content": "Nascondi contenuti",
|
||||
"hide_full_subject": "Nascondi oggetto intero",
|
||||
"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",
|
||||
"mentions": "Menzioni",
|
||||
"move_down": "Muovi allegato a destra",
|
||||
"move_up": "Muovi allegato a sinistra",
|
||||
"mute_conversation": "Silenzia conversazione",
|
||||
"nsfw": "DISDICEVOLE",
|
||||
"pin": "Intesta al profilo",
|
||||
"pinned": "Intestato",
|
||||
"nsfw": "NSFW",
|
||||
"open_gallery": "Apri galleria",
|
||||
"override_translation_source_language": "Sovrascrivi lingua di origine",
|
||||
"pin": "Fissa in cima al profilo",
|
||||
"pinned": "Fissato",
|
||||
"plus_more": "+{number} altri",
|
||||
"repeats": "Condivisi",
|
||||
"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",
|
||||
"replies_list": "Risposte:",
|
||||
"replies_list_with_others": "Mostra {numReplies} altra risposta | Mostra {numReplies} altre risposte",
|
||||
"reply_to": "In risposta a",
|
||||
"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",
|
||||
"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",
|
||||
"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": "Riabilita conversazione",
|
||||
"unpin": "De-intesta",
|
||||
"unmute_conversation": "Desilenzia conversazione",
|
||||
"unpin": "Rimuovi dalla cima del profilo",
|
||||
"you": "(Tu)"
|
||||
},
|
||||
"time": {
|
||||
"in_future": "fra {0}",
|
||||
"in_past": "{0} fa",
|
||||
"now": "adesso",
|
||||
"now_short": "adesso",
|
||||
"now": "proprio adesso",
|
||||
"now_short": "ora",
|
||||
"unit": {
|
||||
"days": "{0} giorni",
|
||||
"days": "{0} giorno | {0} giorni",
|
||||
"days_short": "{0} g",
|
||||
"hours": "{0} ore",
|
||||
"hours_short": "{0} h",
|
||||
"minutes": "{0} minuti",
|
||||
"hours": "{0} ora | {0} ore",
|
||||
"hours_short": "{0} ora | {0} ore",
|
||||
"minutes": "{0} minuto | {0} minuti",
|
||||
"minutes_short": "{0} min",
|
||||
"months": "{0} mesi",
|
||||
"months_short": "{0} mes",
|
||||
"seconds": "{0} secondi",
|
||||
"months": "{0} mese | {0} mesi",
|
||||
"months_short": "{0} mese | {0} mesi",
|
||||
"seconds": "{0} secondo | {0} secondi",
|
||||
"seconds_short": "{0} sec",
|
||||
"weeks": "{0} settimane",
|
||||
"weeks_short": "{0} stm",
|
||||
"years": "{0} anni",
|
||||
"weeks": "{0} settimana | {0} settimane",
|
||||
"weeks_short": "{0} sett",
|
||||
"years": "{0} anno | {0} anni",
|
||||
"years_short": "{0} a"
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "Ripiega",
|
||||
"collapse": "Riduci",
|
||||
"conversation": "Conversazione",
|
||||
"error": "Errore nel caricare la sequenza: {0}",
|
||||
"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",
|
||||
"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",
|
||||
"reload": "Ricarica",
|
||||
"repeated": "ha condiviso",
|
||||
"show_new": "Mostra nuovi",
|
||||
"socket_broke": "Connessione tempo reale interrotta: codice {0}",
|
||||
"socket_broke": "Connessione tempo reale interrotta: CloseEvent 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 seguace",
|
||||
"add_reaction": "Reagisci",
|
||||
"accept_follow_request": "Accetta richiesta di follow",
|
||||
"add_reaction": "Aggiungi reazione",
|
||||
"bookmark": "Aggiungi segnalibro",
|
||||
"favorite": "Gradisci",
|
||||
"media_upload": "Carica allegati",
|
||||
"reject_follow_request": "Rifiuta seguace",
|
||||
"favorite": "Rendi preferito",
|
||||
"media_upload": "Carica media",
|
||||
"quote": "Cita",
|
||||
"reject_follow_request": "Rifiuta richiesta di follow",
|
||||
"repeat": "Condividi",
|
||||
"reply": "Rispondi",
|
||||
"user_settings": "Impostazioni utente"
|
||||
|
|
@ -934,7 +1037,7 @@
|
|||
"upload": {
|
||||
"error": {
|
||||
"base": "Caricamento fallito.",
|
||||
"default": "Riprova in seguito",
|
||||
"default": "Riprova più tardi",
|
||||
"file_too_big": "File troppo pesante [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||
"message": "Caricamento fallito: {0}"
|
||||
},
|
||||
|
|
@ -948,79 +1051,115 @@
|
|||
},
|
||||
"user_card": {
|
||||
"admin_menu": {
|
||||
"activate_account": "Attiva profilo",
|
||||
"deactivate_account": "Disattiva profilo",
|
||||
"delete_account": "Elimina profilo",
|
||||
"activate_account": "Riattiva account",
|
||||
"deactivate_account": "Disattiva account",
|
||||
"delete_account": "Elimina account",
|
||||
"delete_user": "Elimina utente",
|
||||
"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",
|
||||
"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",
|
||||
"moderation": "Moderazione",
|
||||
"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"
|
||||
"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"
|
||||
},
|
||||
"approve": "Approva",
|
||||
"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",
|
||||
"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",
|
||||
"deny": "Nega",
|
||||
"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",
|
||||
"edit_profile": "Modifica profilo",
|
||||
"favorites": "Preferiti",
|
||||
"follow": "Segui",
|
||||
"follow_cancel": "Annulla richiesta",
|
||||
"follow_progress": "Richiedo…",
|
||||
"follow_sent": "Richiesta inviata!",
|
||||
"follow_unfollow": "Disconosci",
|
||||
"followees": "Segue",
|
||||
"followers": "Seguaci",
|
||||
"following": "Seguìto!",
|
||||
"follow_tag": "Segui l'hashtag",
|
||||
"follow_unfollow": "Smetti di seguire",
|
||||
"followed_tags": "Hashtag seguiti",
|
||||
"followed_users": "Utenti seguiti",
|
||||
"followees": "Seguiti",
|
||||
"followers": "Follower",
|
||||
"following": "Seguito!",
|
||||
"follows_you": "Ti segue!",
|
||||
"hidden": "Nascosto",
|
||||
"hide_repeats": "Nascondi condivisioni",
|
||||
"highlight": {
|
||||
"disabled": "Nessun risalto",
|
||||
"side": "Nastro a lato",
|
||||
"solid": "Un colore",
|
||||
"striped": "A righe"
|
||||
"disabled": "Nessuno sfondo",
|
||||
"side": "Striscia laterale",
|
||||
"solid": "Sfondo monocolore",
|
||||
"striped": "Sfondo a righe"
|
||||
},
|
||||
"its_you": "Sei tu!",
|
||||
"media": "Media",
|
||||
"mention": "Menziona",
|
||||
"message": "Contatta",
|
||||
"mute": "Silenzia",
|
||||
"mute_progress": "Silenzio…",
|
||||
"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…",
|
||||
"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": "Messaggi",
|
||||
"subscribe": "Abbònati",
|
||||
"statuses": "Post",
|
||||
"subscribe": "Iscriviti",
|
||||
"unblock": "Sblocca",
|
||||
"unblock_progress": "Sblocco…",
|
||||
"unmute": "Riabilita",
|
||||
"unmute_progress": "Riabilito…",
|
||||
"unsubscribe": "Disdici"
|
||||
"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"
|
||||
},
|
||||
"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 stanza. Puoi motivarla qui sotto:",
|
||||
"additional_comments": "Osservazioni accessorie",
|
||||
"forward_description": "Il profilo appartiene ad un'altra stanza. Inviare la segnalazione anche a quella?",
|
||||
"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?",
|
||||
"forward_to": "Inoltra a {0}",
|
||||
"generic_error": "C'è stato un errore nell'elaborazione della tua richiesta.",
|
||||
"submit": "Invia",
|
||||
"title": "Segnalo {0}"
|
||||
"title": "Segnala {0}"
|
||||
},
|
||||
"who_to_follow": {
|
||||
"more": "Altro",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"about": {
|
||||
"bubble_instances": "ローカルバブルインスタンス",
|
||||
"bubble_instances_description": "かんりしゃがだいひょうするためにえらんだインスタンス",
|
||||
"mrf": {
|
||||
"federation": "フェデレーション",
|
||||
"keyword": {
|
||||
|
|
|
|||
124
src/i18n/lt.json
Normal file
124
src/i18n/lt.json
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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": "Spraw, by konto mogli wyświetlać tylko zatwierdzeni obserwujący",
|
||||
"lock_account_description": "Wymagaj potwierdzenia nowych śledzących",
|
||||
"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,6 +679,7 @@
|
|||
"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",
|
||||
|
|
@ -1148,7 +1149,7 @@
|
|||
"followed_users": "Śledzeni użytkownicy",
|
||||
"followees": "Obserwowani",
|
||||
"followers": "Obserwujący",
|
||||
"following": "Obserwowany!",
|
||||
"following": "Obserwujesz!",
|
||||
"follows_you": "Obserwuje cię!",
|
||||
"hidden": "Ukryte",
|
||||
"hide_repeats": "Ukryj powtórzenia",
|
||||
|
|
|
|||
127
src/i18n/tr.json
127
src/i18n/tr.json
|
|
@ -129,6 +129,131 @@
|
|||
"generic_error": "Bir hata oluştu",
|
||||
"loading": "Yükleniyor…",
|
||||
"more": "Daha",
|
||||
"optional": "Seçenek"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -492,6 +492,7 @@
|
|||
"cGreen": "绿色(转发)",
|
||||
"cOrange": "橙色(喜欢)",
|
||||
"cRed": "红色(取消)",
|
||||
"center_align_bio": "使用户简介中的文本居中",
|
||||
"change_email": "更改邮箱",
|
||||
"change_email_error": "更改你的邮箱时发生错误。",
|
||||
"change_password": "更改密码",
|
||||
|
|
@ -502,6 +503,7 @@
|
|||
"checkboxRadius": "复选框",
|
||||
"collapse_subject": "折叠带内容警告的帖文",
|
||||
"columns": "分栏",
|
||||
"compact_user_info": "空间充足时显示紧凑用户信息",
|
||||
"composing": "写作",
|
||||
"confirm_dialogs": "需要确认当:",
|
||||
"confirm_dialogs_approve_follow": "接受关注请求",
|
||||
|
|
@ -682,6 +684,7 @@
|
|||
"pause_on_unfocused": "在离开页面时暂停时间线推送",
|
||||
"permit_followback_description": "自动批准已关注用户的关注请求",
|
||||
"play_videos_in_modal": "在弹出框内播放视频",
|
||||
"post_language": "默认发布的语言",
|
||||
"post_look_feel": "文章的样子跟感受",
|
||||
"post_status_content_type": "默认发布的内容类型",
|
||||
"posts": "帖文",
|
||||
|
|
@ -947,6 +950,7 @@
|
|||
"title": "版本"
|
||||
},
|
||||
"virtual_scrolling": "优化时间线渲染",
|
||||
"widen_timeline": "加宽时间线以填充水平空间",
|
||||
"word_filter": "词语过滤",
|
||||
"wordfilter": "词语过滤器"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,9 +4,20 @@ 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 ? state : paths.reduce((substate, path) => {
|
||||
set(substate, path, get(state, path))
|
||||
paths.length === 0 ? clonePublicKeys(state) : paths.reduce((substate, path) => {
|
||||
set(substate, path, clonePublicKeys(get(state, path)))
|
||||
return substate
|
||||
}, {})
|
||||
)
|
||||
|
|
@ -19,7 +30,7 @@ const saveImmedeatelyActions = [
|
|||
'setOption',
|
||||
'setClientData',
|
||||
'setToken',
|
||||
'clearToken',
|
||||
'clearTokens',
|
||||
'emojiUsed',
|
||||
]
|
||||
|
||||
|
|
@ -70,7 +81,7 @@ export default function createPersistedState ({
|
|||
subscriber(store)((mutation, state) => {
|
||||
try {
|
||||
if (saveImmedeatelyActions.includes(mutation.type)) {
|
||||
setState(key, reducer(cloneDeep(state), paths), storage)
|
||||
setState(key, reducer(state, paths), storage)
|
||||
.then(success => {
|
||||
if (typeof success !== 'undefined') {
|
||||
if (mutation.type === 'setOption' || mutation.type === 'setCurrentUser') {
|
||||
|
|
|
|||
28
src/lib/scope_utils.js
Normal file
28
src/lib/scope_utils.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
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];
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ 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'
|
||||
|
|
@ -87,6 +88,7 @@ const persistedStateOptions = {
|
|||
users: usersModule,
|
||||
statuses: statusesModule,
|
||||
lists: listsModule,
|
||||
dmConversations: dmConversationsModule,
|
||||
api: apiModule,
|
||||
config: configModule,
|
||||
serverSideConfig: serverSideConfigModule,
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ const api = {
|
|||
// Timelines
|
||||
startFetchingTimeline(
|
||||
store,
|
||||
{ timeline = "friends", tag = false, userId = false, listId = false },
|
||||
{ timeline = "friends", tag = false, userId = false, listId = false, conversationId = false },
|
||||
) {
|
||||
if (store.state.fetchers[timeline]) return;
|
||||
|
||||
|
|
@ -238,6 +238,7 @@ const api = {
|
|||
store,
|
||||
userId,
|
||||
listId,
|
||||
conversationId,
|
||||
tag,
|
||||
});
|
||||
store.commit("addFetcher", { fetcherName: timeline, fetcher });
|
||||
|
|
@ -347,6 +348,20 @@ 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 })
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@ export const defaultState = {
|
|||
autohideFloatingPostButton: false,
|
||||
pauseOnUnfocused: true,
|
||||
displayPageBackgrounds: true,
|
||||
centerAlignBio: false,
|
||||
compactUserInfo: true,
|
||||
stopGifs: undefined,
|
||||
replyVisibility: 'all',
|
||||
thirdColumnMode: 'notifications',
|
||||
|
|
@ -77,6 +79,7 @@ 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
|
||||
|
|
@ -90,6 +93,7 @@ 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,
|
||||
|
|
@ -141,9 +145,38 @@ function updateLocalSettings(store, settingEntries, version = null) {
|
|||
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 },
|
||||
state: { ...defaultState, '__parsed_cache': {} },
|
||||
getters: {
|
||||
defaultConfig (state, getters, rootState, rootGetters) {
|
||||
const { instance } = rootState
|
||||
|
|
@ -158,14 +191,32 @@ const config = {
|
|||
const { defaultConfig } = rootGetters
|
||||
return {
|
||||
...defaultConfig,
|
||||
// Do not override with undefined
|
||||
...Object.fromEntries(Object.entries(state).filter(([k, v]) => v !== undefined))
|
||||
// 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
|
||||
}
|
||||
}
|
||||
},
|
||||
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]
|
||||
|
|
@ -186,8 +237,9 @@ const config = {
|
|||
timeout: 5000
|
||||
}
|
||||
store.dispatch('pushGlobalNotice', notice)
|
||||
let {'__parsed_cache': _, ...currentSettings} = store.state
|
||||
store.rootState.api.backendInteractor.saveSettingsProfile({
|
||||
settings: store.state, profileName: store.state.profile, version: store.state.profileVersion
|
||||
settings: currentSettings, profileName: store.state.profile, version: store.state.profileVersion
|
||||
}).then(() => {
|
||||
store.dispatch('removeGlobalNotice', notice)
|
||||
store.dispatch('pushGlobalNotice', {
|
||||
|
|
|
|||
106
src/modules/dm_conversations.js
Normal file
106
src/modules/dm_conversations.js
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
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
|
||||
|
|
@ -47,6 +47,7 @@ const defaultState = {
|
|||
modalOnLogout: true,
|
||||
modalOnApproveFollow: false,
|
||||
modalOnDenyFollow: false,
|
||||
modalOnDeleteDMConversation: true,
|
||||
loginMethod: 'password',
|
||||
logo: '/static/logo.svg',
|
||||
logoMargin: '.2em',
|
||||
|
|
@ -61,6 +62,7 @@ const defaultState = {
|
|||
showNavShortcuts: true,
|
||||
showWiderShortcuts: true,
|
||||
sidebarRight: false,
|
||||
widenTimeline: false,
|
||||
subjectLineBehavior: 'email',
|
||||
theme: 'pleroma-dark',
|
||||
virtualScrolling: true,
|
||||
|
|
@ -73,6 +75,8 @@ 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: [],
|
||||
|
|
@ -182,10 +186,13 @@ const instance = {
|
|||
const result = await res.json()
|
||||
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
|
||||
const emoji = Object.entries(values).map(([key, value]) => {
|
||||
const imageUrl = value.image_url
|
||||
let imageUrl = value.image_url
|
||||
if (typeof imageUrl == 'string' && imageUrl.startsWith('/'))
|
||||
imageUrl = state.server + imageUrl;
|
||||
|
||||
return {
|
||||
displayText: key,
|
||||
imageUrl: imageUrl ? state.server + imageUrl : value,
|
||||
imageUrl: imageUrl ? imageUrl : value,
|
||||
tags: imageUrl ? value.tags.sort((a, b) => a > b ? 1 : 0) : ['utf'],
|
||||
replacement: `:${key}: `
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,22 +8,33 @@ export const defaultState = {
|
|||
export const mutations = {
|
||||
setLists (state, value) {
|
||||
state.allLists = value
|
||||
|
||||
// Stub out fields for newly fetched lists
|
||||
for (const list of state.allLists) {
|
||||
if (!list.accountIds) list.accountIds = []
|
||||
}
|
||||
},
|
||||
setList (state, { id, title }) {
|
||||
if (!state.allListsObject[id]) {
|
||||
state.allListsObject[id] = {}
|
||||
}
|
||||
state.allListsObject[id].title = title
|
||||
|
||||
if (!find(state.allLists, { id })) {
|
||||
state.allLists.push({ id, title })
|
||||
const list = state.allListsObject[id]
|
||||
list.title = title
|
||||
// newly created list
|
||||
if (!list.accountIds) list.accountIds = []
|
||||
|
||||
const listEntry = find(state.allLists, { id })
|
||||
if (!listEntry) {
|
||||
state.allLists.push({ id, ...list })
|
||||
} else {
|
||||
find(state.allLists, { id }).title = title
|
||||
Object.assign(listEntry, list)
|
||||
}
|
||||
},
|
||||
setListAccounts (state, { id, accountIds }) {
|
||||
// XXX: this shouldn’t happen in the first place...
|
||||
if (!state.allListsObject[id]) {
|
||||
state.allListsObject[id] = {}
|
||||
state.allListsObject[id] = { title: "" }
|
||||
}
|
||||
state.allListsObject[id].accountIds = accountIds
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@ const oauth = {
|
|||
clientId: false,
|
||||
clientSecret: false,
|
||||
/* App token is authentication for app without any user, used mostly for
|
||||
* MastoAPI's registration of new users, stored so that we can fall back to
|
||||
* it on logout
|
||||
* MastoAPI's registration of new users and unnecessary otherwise
|
||||
*/
|
||||
appToken: false,
|
||||
/* User token is authentication for app with user, this is for every calls
|
||||
|
|
@ -23,8 +22,9 @@ const oauth = {
|
|||
setToken (state, token) {
|
||||
state.userToken = token
|
||||
},
|
||||
clearToken (state) {
|
||||
clearTokens (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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { merge } from 'lodash'
|
||||
|
||||
const POLL_UPDATE_FREQUENCY = 150_000;
|
||||
|
||||
const polls = {
|
||||
state: {
|
||||
// Contains key = id, value = number of trackers for this poll
|
||||
|
|
@ -12,6 +14,9 @@ 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
|
||||
|
|
@ -44,13 +49,16 @@ const polls = {
|
|||
if (rootState.polls.trackedPolls[pollId]) {
|
||||
dispatch('updateTrackedPoll', pollId)
|
||||
}
|
||||
}, 30 * 1000)
|
||||
}, POLL_UPDATE_FREQUENCY)
|
||||
commit('mergeOrAddPoll', poll)
|
||||
})
|
||||
},
|
||||
trackPoll ({ rootState, commit, dispatch }, pollId) {
|
||||
if (rootState.polls.pollsObject[pollId]?.expired)
|
||||
return;
|
||||
|
||||
if (!rootState.polls.trackedPolls[pollId]) {
|
||||
setTimeout(() => dispatch('updateTrackedPoll', pollId), 30 * 1000)
|
||||
setTimeout(() => dispatch('updateTrackedPoll', pollId), POLL_UPDATE_FREQUENCY)
|
||||
}
|
||||
commit('trackPoll', pollId)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,14 +205,6 @@ 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.
|
||||
|
|
@ -663,10 +655,10 @@ const statuses = {
|
|||
return rootState.api.backendInteractor.unmuteConversation({ id: statusId })
|
||||
.then((status) => commit('setMutedStatus', status))
|
||||
},
|
||||
retweet ({ rootState, commit }, status) {
|
||||
retweet ({ rootState, commit }, {id, visibility}) {
|
||||
// Optimistic retweeting...
|
||||
commit('setRetweeted', { status, value: true })
|
||||
rootState.api.backendInteractor.retweet({ id: status.id })
|
||||
commit('setRetweeted', { status: {id: id}, value: true })
|
||||
rootState.api.backendInteractor.retweet({ id: id, visibility: visibility })
|
||||
.then(status => commit('setRetweetedConfirm', { status: status.retweeted_status, user: rootState.users.currentUser }))
|
||||
},
|
||||
unretweet ({ rootState, commit }, status) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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') => {
|
||||
|
|
@ -30,12 +31,12 @@ const mergeArrayLength = (oldValue, newValue) => {
|
|||
}
|
||||
}
|
||||
|
||||
const getNotificationPermission = () => {
|
||||
const getNotificationPermission = async () => {
|
||||
const Notification = window.Notification
|
||||
|
||||
if (!Notification) return Promise.resolve(null)
|
||||
if (Notification.permission === 'default') return Notification.requestPermission()
|
||||
return Promise.resolve(Notification.permission)
|
||||
if (!Notification) return null
|
||||
if (Notification.permission === 'default') return await Notification.requestPermission()
|
||||
return Notification.permission
|
||||
}
|
||||
|
||||
const blockUser = (store, id) => {
|
||||
|
|
@ -112,6 +113,17 @@ 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]
|
||||
|
|
@ -143,6 +155,10 @@ 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
|
||||
|
|
@ -277,6 +293,14 @@ 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--
|
||||
},
|
||||
|
|
@ -568,6 +592,8 @@ 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 } }
|
||||
)
|
||||
|
|
@ -603,18 +629,25 @@ const users = {
|
|||
|
||||
return oauthApi.getOrCreateApp(data)
|
||||
.then((app) => {
|
||||
const params = {
|
||||
app,
|
||||
instance: data.instance,
|
||||
token: oauth.userToken
|
||||
}
|
||||
// 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
|
||||
|
||||
return oauthApi.revokeToken(params)
|
||||
const params = {
|
||||
app,
|
||||
instance: data.instance,
|
||||
token: token
|
||||
}
|
||||
|
||||
oauthApi.revokeToken(params)
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
store.dispatch('stopFetchingCurrentUserInfo')
|
||||
store.commit('clearCurrentUser')
|
||||
store.dispatch('disconnectFromSocket')
|
||||
store.commit('clearToken')
|
||||
store.commit('clearTokens')
|
||||
store.dispatch('stopFetchingTimeline', 'friends')
|
||||
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||
store.dispatch('stopFetchingNotifications')
|
||||
|
|
@ -683,6 +716,7 @@ 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')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
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;
|
||||
|
|
@ -60,7 +59,6 @@
|
|||
padding: 0.6em;
|
||||
height: var(--__panel-heading-height);
|
||||
line-height: var(--__panel-heading-height-inner);
|
||||
z-index: 4;
|
||||
|
||||
&.-flexible-height {
|
||||
--__panel-heading-height: auto;
|
||||
|
|
@ -131,6 +129,7 @@
|
|||
color: var(--panelText);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
z-index: 4;
|
||||
|
||||
&::after {
|
||||
background-color: $fallback--fg;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { each, map, concat, last, get } from 'lodash'
|
||||
import { parseStatus, parseSource, parseUser, parseNotification, parseReport, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
import { parseStatus, parseSource, parseUser, parseNotification, parseReport, parseAttachment, parseDMConversation, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
import { RegistrationError, StatusCodeError } from '../errors/errors'
|
||||
import { Url } from 'url'
|
||||
|
||||
|
|
@ -50,10 +50,15 @@ 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`
|
||||
|
|
@ -124,6 +129,21 @@ 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,
|
||||
|
|
@ -135,7 +155,13 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers =
|
|||
}
|
||||
if (params) {
|
||||
url += '?' + Object.entries(params)
|
||||
.map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value))
|
||||
.map(([key, value]) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return (encodeURIComponent(key) + '=' + encodeURIComponent(value))
|
||||
} else {
|
||||
return value.map((v) => encodeURIComponent(key) + '[]=' + encodeURIComponent(v)).join('&')
|
||||
}
|
||||
})
|
||||
.join('&')
|
||||
}
|
||||
if (payload) {
|
||||
|
|
@ -148,19 +174,7 @@ const promisedRequest = ({ method, url, params, payload, credentials, headers =
|
|||
}
|
||||
}
|
||||
return fetch(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))
|
||||
})
|
||||
)
|
||||
})
|
||||
.then((response) => getJsonIfSuccess(response, url, options))
|
||||
}
|
||||
|
||||
const updateNotificationSettings = ({ credentials, settings }) => {
|
||||
|
|
@ -511,6 +525,80 @@ 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) })
|
||||
|
|
@ -693,6 +781,7 @@ const fetchTimeline = ({
|
|||
until = false,
|
||||
userId = false,
|
||||
listId = false,
|
||||
conversationId = false,
|
||||
tag = false,
|
||||
withMuted = false,
|
||||
replyVisibility = 'all'
|
||||
|
|
@ -701,7 +790,7 @@ const fetchTimeline = ({
|
|||
public: MASTODON_PUBLIC_TIMELINE,
|
||||
bubble: AKKOMA_BUBBLE_TIMELINE_URL,
|
||||
friends: MASTODON_USER_HOME_TIMELINE_URL,
|
||||
dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
|
||||
dmConv: PLEROMA_CONVERSATION_TIMELINE,
|
||||
notifications: MASTODON_USER_NOTIFICATIONS_URL,
|
||||
'publicAndExternal': MASTODON_PUBLIC_TIMELINE,
|
||||
user: MASTODON_USER_TIMELINE_URL,
|
||||
|
|
@ -725,6 +814,10 @@ const fetchTimeline = ({
|
|||
url = url(listId)
|
||||
}
|
||||
|
||||
if (timeline === 'dmConv') {
|
||||
url = url(conversationId)
|
||||
}
|
||||
|
||||
if (since) {
|
||||
params.push(['since_id', since])
|
||||
}
|
||||
|
|
@ -822,8 +915,8 @@ const unfavorite = ({ id, credentials }) => {
|
|||
.then((data) => parseStatus(data))
|
||||
}
|
||||
|
||||
const retweet = ({ id, credentials }) => {
|
||||
return promisedRequest({ url: MASTODON_RETWEET_URL(id), method: 'POST', credentials })
|
||||
const retweet = ({ id, visibility, credentials }) => {
|
||||
return promisedRequest({ url: MASTODON_RETWEET_URL(id), method: 'POST', payload: { visibility }, credentials })
|
||||
.then((data) => parseStatus(data))
|
||||
}
|
||||
|
||||
|
|
@ -988,7 +1081,7 @@ const uploadMedia = ({ formData, credentials }) => {
|
|||
method: 'POST',
|
||||
headers: authHeaders(credentials)
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.then((response) => getJsonIfSuccess(response, MASTODON_MEDIA_UPLOAD_URL, {}))
|
||||
.then((data) => parseAttachment(data))
|
||||
}
|
||||
|
||||
|
|
@ -1588,7 +1681,7 @@ const getFollowedHashtags = ({ credentials, pagination: savedPagination }) => {
|
|||
const url = `${MASTODON_FOLLOWED_TAGS_URL}?${queryParams.toString()}`
|
||||
let pagination = {};
|
||||
return fetch(url, {
|
||||
credentials
|
||||
headers: authHeaders(credentials),
|
||||
}).then((data) => {
|
||||
pagination = parseLinkHeaderPagination(data.headers.get('Link'), {
|
||||
flakeId: false
|
||||
|
|
@ -1610,7 +1703,7 @@ const getFollowRequests = ({ credentials, pagination: savedPagination }) => {
|
|||
const url = `${MASTODON_FOLLOW_REQUESTS_URL}?${queryParams.toString()}`
|
||||
let pagination = {};
|
||||
return fetch(url, {
|
||||
credentials
|
||||
headers: authHeaders(credentials),
|
||||
}).then((data) => {
|
||||
pagination = parseLinkHeaderPagination(data.headers.get('Link'), { flakeId: true });
|
||||
return data.json()
|
||||
|
|
@ -1746,6 +1839,12 @@ 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
Loading…
Add table
Add a link
Reference in a new issue