mirror of
https://akkoma.dev/lamp/akkoma-fe.git
synced 2026-06-04 22:30:04 -04:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d91a7b2c3 | |||
| ed0b403c33 | |||
| 0f842b300b | |||
| 865cb6f96a | |||
| 050c7df2e6 | |||
| a77a9e04d9 | |||
| a57334991e | |||
| 8dce31d0ad | |||
| ea9ad4d600 | |||
| 34e2800f59 | |||
| 3d65eccf04 | |||
| d304be654f | |||
| aee97fa948 | |||
| 7da1687f31 | |||
| a8f193d4bd | |||
| 81c82e11bc | |||
| 00cadce5b4 | |||
| 40a08f279b | |||
| c524a47e6f | |||
| 235c734d37 | |||
| deaef1d0b9 | |||
| 1b28ec3b72 | |||
| c9dc8f00f9 | |||
| beee99e733 | |||
| ccb0ffdc8a | |||
| ab250c2f3a | |||
| 1de62fffcd | |||
| 306cea04a1 | |||
| d9e1bc4d99 | |||
| 52b0b6f008 | |||
| 8afbe5e3bc | |||
| 58be48d164 | |||
| 1056b89fd1 | |||
| 3e64d78d05 | |||
| 3947aafeba | |||
| 345934c2f3 | |||
| 42a13b0f1b | |||
| e13c4b6b85 | |||
| 6a1409e09b | |||
| 174f98b1cb |
@@ -1,3 +1,4 @@
|
|||||||
|
platform: linux/amd64
|
||||||
pipeline:
|
pipeline:
|
||||||
lint:
|
lint:
|
||||||
when:
|
when:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/ad
|
|||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
# install dependencies
|
# install dependencies
|
||||||
npm install -g yarn
|
corepack enable
|
||||||
yarn
|
yarn
|
||||||
|
|
||||||
# serve with hot reload at localhost:8080
|
# serve with hot reload at localhost:8080
|
||||||
@@ -37,7 +37,7 @@ npm run unit
|
|||||||
|
|
||||||
# For Contributors:
|
# For Contributors:
|
||||||
|
|
||||||
You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/config/local.example.json)) to enable some convenience dev options:
|
You can create file `/config/local.json` (see [example](https://akkoma.dev/AkkomaGang/akkoma-fe/src/branch/develop/config/local.example.json)) to enable some convenience dev options:
|
||||||
|
|
||||||
* `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases.
|
* `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases.
|
||||||
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.
|
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"target": "https://pleroma.soykaf.com/",
|
"target": "https://otp.akkoma.dev/",
|
||||||
"staticConfigPreference": false
|
"staticConfigPreference": false
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -64,6 +64,11 @@ export default {
|
|||||||
'-' + this.layoutType
|
'-' + this.layoutType
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
pageBackground () {
|
||||||
|
return this.mergedConfig.displayPageBackgrounds
|
||||||
|
? this.$store.state.users.displayBackground
|
||||||
|
: null
|
||||||
|
},
|
||||||
currentUser () { return this.$store.state.users.currentUser },
|
currentUser () { return this.$store.state.users.currentUser },
|
||||||
userBackground () { return this.currentUser.background_image },
|
userBackground () { return this.currentUser.background_image },
|
||||||
instanceBackground () {
|
instanceBackground () {
|
||||||
@@ -71,7 +76,7 @@ export default {
|
|||||||
? null
|
? null
|
||||||
: this.$store.state.instance.background
|
: this.$store.state.instance.background
|
||||||
},
|
},
|
||||||
background () { return this.userBackground || this.instanceBackground },
|
background () { return this.pageBackground || this.userBackground || this.instanceBackground },
|
||||||
bgStyle () {
|
bgStyle () {
|
||||||
if (this.background) {
|
if (this.background) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-size: 14px;
|
font-size: 0.875rem;
|
||||||
// overflow-x: clip causes my browser's tab to crash with SIGILL lul
|
// overflow-x: clip causes my browser's tab to crash with SIGILL lul
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: scroll;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.-static {
|
&.-static {
|
||||||
|
|||||||
@@ -93,9 +93,6 @@ const MentionLink = {
|
|||||||
this.highlightType
|
this.highlightType
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
useAtIcon () {
|
|
||||||
return this.mergedConfig.useAtIcon
|
|
||||||
},
|
|
||||||
isRemote () {
|
isRemote () {
|
||||||
return this.userName !== this.userNameFull
|
return this.userName !== this.userNameFull
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -151,7 +151,6 @@
|
|||||||
>
|
>
|
||||||
<Timeago
|
<Timeago
|
||||||
:time="notification.created_at"
|
:time="notification.created_at"
|
||||||
:with-direction="true"
|
|
||||||
:auto-update="240"
|
:auto-update="240"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|||||||
@@ -105,9 +105,12 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
padding-left: 0.8em;
|
padding-left: 0.8em;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-right, .notification-right {
|
||||||
.timeago {
|
.timeago {
|
||||||
min-width: 3em;
|
display: inline-block;
|
||||||
|
min-width: 6em;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ const PostStatusForm = {
|
|||||||
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage } = this.$store.getters.mergedConfig
|
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage, alwaysShowSubjectInput } = this.$store.getters.mergedConfig
|
||||||
|
|
||||||
let statusParams = {
|
let statusParams = {
|
||||||
spoilerText: this.subject || '',
|
spoilerText: this.subject || '',
|
||||||
@@ -199,6 +199,10 @@ const PostStatusForm = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When first loading the form, hide the subject (CW) field if it's disabled or doesn't have a starting value.
|
||||||
|
// "disableSubject" seems to take priority over "alwaysShowSubjectInput".
|
||||||
|
const showSubject = !this.disableSubject && (statusParams.spoilerText || alwaysShowSubjectInput)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dropFiles: [],
|
dropFiles: [],
|
||||||
uploadingFiles: false,
|
uploadingFiles: false,
|
||||||
@@ -213,7 +217,10 @@ const PostStatusForm = {
|
|||||||
preview: null,
|
preview: null,
|
||||||
previewLoading: false,
|
previewLoading: false,
|
||||||
emojiInputShown: false,
|
emojiInputShown: false,
|
||||||
idempotencyKey: ''
|
idempotencyKey: '',
|
||||||
|
activeEmojiInput: undefined,
|
||||||
|
activeTextInput: undefined,
|
||||||
|
subjectVisible: showSubject
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -674,8 +681,33 @@ const PostStatusForm = {
|
|||||||
this.$refs['emoji-input'].resize()
|
this.$refs['emoji-input'].resize()
|
||||||
},
|
},
|
||||||
showEmojiPicker () {
|
showEmojiPicker () {
|
||||||
this.$refs['textarea'].focus()
|
if (!this.activeEmojiInput || !this.activeTextInput)
|
||||||
this.$refs['emoji-input'].triggerShowPicker()
|
this.focusStatusInput()
|
||||||
|
|
||||||
|
this.$refs[this.activeTextInput].focus()
|
||||||
|
this.$refs[this.activeEmojiInput].triggerShowPicker()
|
||||||
|
},
|
||||||
|
focusStatusInput() {
|
||||||
|
this.activeEmojiInput = 'emoji-input'
|
||||||
|
this.activeTextInput = 'textarea'
|
||||||
|
},
|
||||||
|
focusSubjectInput() {
|
||||||
|
this.activeEmojiInput = 'subject-emoji-input'
|
||||||
|
this.activeTextInput = 'subject-input'
|
||||||
|
},
|
||||||
|
toggleSubjectVisible() {
|
||||||
|
// If hiding CW, then we need to clear the subject and reset focus
|
||||||
|
if (this.subjectVisible)
|
||||||
|
{
|
||||||
|
this.focusStatusInput()
|
||||||
|
|
||||||
|
// "nsfw" property is normally set by the @change listener, but this bypasses it.
|
||||||
|
// We need to clear it manually instead.
|
||||||
|
this.newStatus.spoilerText = ''
|
||||||
|
this.newStatus.nsfw = false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.subjectVisible = !this.subjectVisible
|
||||||
},
|
},
|
||||||
clearError () {
|
clearError () {
|
||||||
this.error = null
|
this.error = null
|
||||||
|
|||||||
@@ -118,13 +118,16 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<EmojiInput
|
<EmojiInput
|
||||||
v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
|
ref="subject-emoji-input"
|
||||||
|
v-if="subjectVisible"
|
||||||
v-model="newStatus.spoilerText"
|
v-model="newStatus.spoilerText"
|
||||||
enable-emoji-picker
|
enable-emoji-picker
|
||||||
|
hide-emoji-button
|
||||||
:suggest="emojiSuggestor"
|
:suggest="emojiSuggestor"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
ref="subject-input"
|
||||||
v-model="newStatus.spoilerText"
|
v-model="newStatus.spoilerText"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('post_status.content_warning')"
|
:placeholder="$t('post_status.content_warning')"
|
||||||
@@ -132,6 +135,7 @@
|
|||||||
size="1"
|
size="1"
|
||||||
class="form-post-subject"
|
class="form-post-subject"
|
||||||
@input="onSubjectInput"
|
@input="onSubjectInput"
|
||||||
|
@focus="focusSubjectInput()"
|
||||||
>
|
>
|
||||||
</EmojiInput>
|
</EmojiInput>
|
||||||
<i18n-t
|
<i18n-t
|
||||||
@@ -173,6 +177,7 @@
|
|||||||
@input="resize"
|
@input="resize"
|
||||||
@compositionupdate="resize"
|
@compositionupdate="resize"
|
||||||
@paste="paste"
|
@paste="paste"
|
||||||
|
@focus="focusStatusInput()"
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
v-if="hasStatusLengthLimit"
|
v-if="hasStatusLengthLimit"
|
||||||
@@ -276,6 +281,15 @@
|
|||||||
>
|
>
|
||||||
<FAIcon icon="poll-h" />
|
<FAIcon icon="poll-h" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="!disableSubject"
|
||||||
|
class="spoiler-icon button-unstyled"
|
||||||
|
:class="{ selected: subjectVisible }"
|
||||||
|
:title="$t('post_status.toggle_content_warning')"
|
||||||
|
@click="toggleSubjectVisible"
|
||||||
|
>
|
||||||
|
<FAIcon icon="eye-slash" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-if="posting"
|
v-if="posting"
|
||||||
@@ -456,7 +470,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-upload-icon, .poll-icon, .emoji-icon {
|
.media-upload-icon, .poll-icon, .emoji-icon, .spoiler-icon {
|
||||||
font-size: 1.85em;
|
font-size: 1.85em;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -499,6 +513,11 @@
|
|||||||
|
|
||||||
.poll-icon {
|
.poll-icon {
|
||||||
order: 3;
|
order: 3;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler-icon {
|
||||||
|
order: 4;
|
||||||
justify-content: right;
|
justify-content: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -146,6 +146,11 @@
|
|||||||
{{ $t('settings.show_wider_shortcuts') }}
|
{{ $t('settings.show_wider_shortcuts') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting path="displayPageBackgrounds">
|
||||||
|
{{ $t('settings.show_page_backgrounds') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="stopGifs">
|
<BooleanSetting path="stopGifs">
|
||||||
{{ $t('settings.stop_gifs') }}
|
{{ $t('settings.stop_gifs') }}
|
||||||
@@ -483,14 +488,6 @@
|
|||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<li>
|
|
||||||
<BooleanSetting
|
|
||||||
path="useAtIcon"
|
|
||||||
expert="1"
|
|
||||||
>
|
|
||||||
{{ $t('settings.use_at_icon') }}
|
|
||||||
</BooleanSetting>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="mentionLinkShowAvatar">
|
<BooleanSetting path="mentionLinkShowAvatar">
|
||||||
{{ $t('settings.mention_link_show_avatar') }}
|
{{ $t('settings.mention_link_show_avatar') }}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const ProfileTab = {
|
|||||||
newName: this.$store.state.users.currentUser.name_unescaped,
|
newName: this.$store.state.users.currentUser.name_unescaped,
|
||||||
newBio: unescape(this.$store.state.users.currentUser.description),
|
newBio: unescape(this.$store.state.users.currentUser.description),
|
||||||
newLocked: this.$store.state.users.currentUser.locked,
|
newLocked: this.$store.state.users.currentUser.locked,
|
||||||
|
newPermitFollowback: this.$store.state.users.currentUser.permit_followback,
|
||||||
newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
|
newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
|
||||||
showRole: this.$store.state.users.currentUser.show_role,
|
showRole: this.$store.state.users.currentUser.show_role,
|
||||||
role: this.$store.state.users.currentUser.role,
|
role: this.$store.state.users.currentUser.role,
|
||||||
@@ -135,6 +136,7 @@ const ProfileTab = {
|
|||||||
bot: this.bot,
|
bot: this.bot,
|
||||||
show_role: this.showRole,
|
show_role: this.showRole,
|
||||||
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1,
|
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1,
|
||||||
|
permit_followback: this.permit_followback,
|
||||||
accepts_direct_messages_from: this.userAcceptsDirectMessagesFrom
|
accepts_direct_messages_from: this.userAcceptsDirectMessagesFrom
|
||||||
/* eslint-enable camelcase */
|
/* eslint-enable camelcase */
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -259,6 +259,19 @@
|
|||||||
<BooleanSetting path="serverSide_locked">
|
<BooleanSetting path="serverSide_locked">
|
||||||
{{ $t('settings.lock_account_description') }}
|
{{ $t('settings.lock_account_description') }}
|
||||||
</BooleanSetting>
|
</BooleanSetting>
|
||||||
|
<ul
|
||||||
|
class="setting-list suboptions"
|
||||||
|
:class="[{disabled: !serverSide_locked}]"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<BooleanSetting
|
||||||
|
path="serverSide_permitFollowback"
|
||||||
|
:disabled="!serverSide_locked"
|
||||||
|
>
|
||||||
|
{{ $t('settings.permit_followback_description') }}
|
||||||
|
</BooleanSetting>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<BooleanSetting path="serverSide_discoverable">
|
<BooleanSetting path="serverSide_discoverable">
|
||||||
|
|||||||
@@ -190,7 +190,7 @@
|
|||||||
>
|
>
|
||||||
<Timeago
|
<Timeago
|
||||||
:time="status.created_at"
|
:time="status.created_at"
|
||||||
:with-direction="true"
|
:with-direction="!compact"
|
||||||
:auto-update="60"
|
:auto-update="60"
|
||||||
/>
|
/>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const StillImage = {
|
|||||||
return {
|
return {
|
||||||
stopGifs: this.$store.getters.mergedConfig.stopGifs || window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
stopGifs: this.$store.getters.mergedConfig.stopGifs || window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
||||||
isAnimated: false,
|
isAnimated: false,
|
||||||
|
imageTypeLabel: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -39,27 +40,24 @@ const StillImage = {
|
|||||||
this.imageLoadError && this.imageLoadError()
|
this.imageLoadError && this.imageLoadError()
|
||||||
},
|
},
|
||||||
detectAnimation (image) {
|
detectAnimation (image) {
|
||||||
// If there are no file extensions, the mimetype isn't set, and no mediaproxy is available, we can't figure out
|
|
||||||
// the mimetype of the image.
|
|
||||||
const hasFileExtension = this.src.split('/').pop().includes('.') // TODO: Better check?
|
|
||||||
const mediaProxyAvailable = this.$store.state.instance.mediaProxyAvailable
|
const mediaProxyAvailable = this.$store.state.instance.mediaProxyAvailable
|
||||||
if (!hasFileExtension && this.mimetype === undefined && !mediaProxyAvailable) {
|
|
||||||
|
if (!mediaProxyAvailable) {
|
||||||
// It's a bit aggressive to assume all images we can't find the mimetype of is animated, but necessary for
|
// It's a bit aggressive to assume all images we can't find the mimetype of is animated, but necessary for
|
||||||
// people in need of reduced motion accessibility. As such, we'll consider those images animated if the user
|
// people in need of reduced motion accessibility. As such, we'll consider those images animated if the user
|
||||||
// agent is set to prefer reduced motion. Otherwise, it'll just be used as an early exit.
|
// agent is set to prefer reduced motion. Otherwise, it'll just be used as an early exit.
|
||||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches)
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||||||
|
// Since the canvas and images are not pixel-perfect matching (due to scaling),
|
||||||
|
// It makes the images jiggle on hover, which is not ideal for accessibility, methinks
|
||||||
this.isAnimated = true
|
this.isAnimated = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
this.detectWithoutMediaProxy(image)
|
||||||
if (this.mimetype === 'image/gif' || this.src.endsWith('.gif')) {
|
} else {
|
||||||
this.isAnimated = true
|
this.detectWithMediaProxy(image)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// harmless CORS errors without-- clean console with
|
},
|
||||||
if (!mediaProxyAvailable) return
|
detectAnimationWithFetch (image) {
|
||||||
// Animated JPEGs?
|
|
||||||
if (!(this.src.endsWith('.webp') || this.src.endsWith('.png'))) return
|
|
||||||
// Browser Cache should ensure image doesn't get loaded twice if cache exists
|
// Browser Cache should ensure image doesn't get loaded twice if cache exists
|
||||||
fetch(image.src, {
|
fetch(image.src, {
|
||||||
referrerPolicy: 'same-origin'
|
referrerPolicy: 'same-origin'
|
||||||
@@ -68,12 +66,20 @@ const StillImage = {
|
|||||||
// We don't need to read the whole file so only call it once
|
// We don't need to read the whole file so only call it once
|
||||||
data.body.getReader().read()
|
data.body.getReader().read()
|
||||||
.then(reader => {
|
.then(reader => {
|
||||||
if (this.src.endsWith('.webp') && this.isAnimatedWEBP(reader.value)) {
|
// Ordered from least to most intensive
|
||||||
|
if (this.isGIF(reader.value)) {
|
||||||
this.isAnimated = true
|
this.isAnimated = true
|
||||||
|
this.setLabel('GIF')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (this.src.endsWith('.png') && this.isAnimatedPNG(reader.value)) {
|
if (this.isAnimatedWEBP(reader.value)) {
|
||||||
this.isAnimated = true
|
this.isAnimated = true
|
||||||
|
this.setLabel('WEBP')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.isAnimatedPNG(reader.value)) {
|
||||||
|
this.isAnimated = true
|
||||||
|
this.setLabel('APNG')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -81,6 +87,53 @@ const StillImage = {
|
|||||||
// this.imageLoadError && this.imageLoadError()
|
// this.imageLoadError && this.imageLoadError()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
detectWithMediaProxy (image) {
|
||||||
|
this.detectAnimationWithFetch(image)
|
||||||
|
},
|
||||||
|
detectWithoutMediaProxy (image) {
|
||||||
|
// We'll just assume that gifs and webp are animated
|
||||||
|
const extension = image.src.split('.').pop().toLowerCase()
|
||||||
|
|
||||||
|
if (extension === 'gif') {
|
||||||
|
this.isAnimated = true
|
||||||
|
this.setLabel('GIF')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (extension === 'webp') {
|
||||||
|
this.isAnimated = true
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setLabel (name) {
|
||||||
|
this.imageTypeLabel = name;
|
||||||
|
},
|
||||||
|
isGIF (data) {
|
||||||
|
// I am a perfectly sane individual
|
||||||
|
//
|
||||||
|
// GIF HEADER CHUNK
|
||||||
|
// === START HEADER ===
|
||||||
|
// 47 49 46 38 ("GIF8")
|
||||||
|
const gifHeader = [0x47, 0x49, 0x46];
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
if (data[i] !== gifHeader[i]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
isAnimatedWEBP (data) {
|
isAnimatedWEBP (data) {
|
||||||
/**
|
/**
|
||||||
* WEBP HEADER CHUNK
|
* WEBP HEADER CHUNK
|
||||||
@@ -114,15 +167,54 @@ const StillImage = {
|
|||||||
const idatPos = str.indexOf('IDAT')
|
const idatPos = str.indexOf('IDAT')
|
||||||
return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0)
|
return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0)
|
||||||
},
|
},
|
||||||
drawThumbnail () {
|
drawThumbnail() {
|
||||||
const canvas = this.$refs.canvas
|
const canvas = this.$refs.canvas;
|
||||||
if (!this.$refs.canvas) return
|
if (!canvas) return;
|
||||||
const image = this.$refs.src
|
|
||||||
const width = image.naturalWidth
|
const context = canvas.getContext('2d');
|
||||||
const height = image.naturalHeight
|
const image = this.$refs.src;
|
||||||
canvas.width = width
|
const parentElement = canvas.parentElement;
|
||||||
canvas.height = height
|
|
||||||
canvas.getContext('2d').drawImage(image, 0, 0, width, height)
|
// Draw the quick, unscaled version first
|
||||||
|
context.drawImage(image, 0, 0, parentElement.clientWidth, parentElement.clientHeight);
|
||||||
|
|
||||||
|
// Use requestAnimationFrame to schedule the scaling to the next frame
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
// Compute scaling ratio between the natural dimensions of the image and its display dimensions
|
||||||
|
const scalingRatioWidth = parentElement.clientWidth / image.naturalWidth;
|
||||||
|
const scalingRatioHeight = parentElement.clientHeight / image.naturalHeight;
|
||||||
|
|
||||||
|
// Adjust for high-DPI displays
|
||||||
|
const ratio = window.devicePixelRatio || 1;
|
||||||
|
canvas.width = image.naturalWidth * scalingRatioWidth * ratio;
|
||||||
|
canvas.height = image.naturalHeight * scalingRatioHeight * ratio;
|
||||||
|
canvas.style.width = `${parentElement.clientWidth}px`;
|
||||||
|
canvas.style.height = `${parentElement.clientHeight}px`;
|
||||||
|
context.scale(ratio, ratio);
|
||||||
|
|
||||||
|
// Maintain the aspect ratio of the image
|
||||||
|
const imgAspectRatio = image.naturalWidth / image.naturalHeight;
|
||||||
|
const canvasAspectRatio = parentElement.clientWidth / parentElement.clientHeight;
|
||||||
|
|
||||||
|
let drawWidth, drawHeight;
|
||||||
|
|
||||||
|
if (imgAspectRatio > canvasAspectRatio) {
|
||||||
|
drawWidth = parentElement.clientWidth;
|
||||||
|
drawHeight = parentElement.clientWidth / imgAspectRatio;
|
||||||
|
} else {
|
||||||
|
drawHeight = parentElement.clientHeight;
|
||||||
|
drawWidth = parentElement.clientHeight * imgAspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.clearRect(0, 0, canvas.width, canvas.height); // Clear the previous unscaled image
|
||||||
|
context.imageSmoothingEnabled = true;
|
||||||
|
context.imageSmoothingQuality = 'high';
|
||||||
|
|
||||||
|
// Draw the good one for realsies
|
||||||
|
const dx = (parentElement.clientWidth - drawWidth) / 2;
|
||||||
|
const dy = (parentElement.clientHeight - drawHeight) / 2;
|
||||||
|
context.drawImage(image, dx, dy, drawWidth, drawHeight);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updated () {
|
updated () {
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
|
ref="still-image"
|
||||||
class="still-image"
|
class="still-image"
|
||||||
:class="{ animated: animated }"
|
:class="{ animated: animated }"
|
||||||
:style="style"
|
:style="style"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
v-if="animated && imageTypeLabel"
|
||||||
|
class="image-type-label">
|
||||||
|
{{ imageTypeLabel }}
|
||||||
|
</div>
|
||||||
<canvas
|
<canvas
|
||||||
v-if="animated"
|
v-if="animated"
|
||||||
ref="canvas"
|
ref="canvas"
|
||||||
@@ -57,30 +63,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.animated {
|
.image-type-label {
|
||||||
&::before {
|
|
||||||
zoom: var(--_still_image-label-scale, 1);
|
|
||||||
content: 'gif';
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 0.25em;
|
||||||
|
left: 0.25em;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
font-size: 0.7em;
|
font-size: 0.6em;
|
||||||
top: 0.5em;
|
|
||||||
left: 0.5em;
|
|
||||||
background: rgba(127, 127, 127, 0.5);
|
background: rgba(127, 127, 127, 0.5);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
display: block;
|
|
||||||
padding: 2px 4px;
|
padding: 2px 4px;
|
||||||
border-radius: $fallback--tooltipRadius;
|
|
||||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
visibility: var(--_still-image-label-visibility, visible);
|
visibility: var(--_still-image-label-visibility, visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.animated {
|
||||||
&:hover canvas {
|
&:hover canvas {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover::before {
|
&:hover .image-type-label {
|
||||||
visibility: var(--_still-image-label-visibility, hidden);
|
visibility: var(--_still-image-label-visibility, hidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
--_avatarShadowBox: var(--avatarStatusShadow);
|
--_avatarShadowBox: var(--avatarStatusShadow);
|
||||||
--_avatarShadowFilter: var(--avatarStatusShadowFilter);
|
--_avatarShadowFilter: var(--avatarStatusShadowFilter);
|
||||||
--_avatarShadowInset: var(--avatarStatusShadowInset);
|
--_avatarShadowInset: var(--avatarStatusShadowInset);
|
||||||
--_still-image-label-visibility: hidden;
|
// --_still-image-label-visibility: hidden;
|
||||||
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ const UserProfile = {
|
|||||||
followersTabVisible () {
|
followersTabVisible () {
|
||||||
return this.isUs || !this.user.hide_followers
|
return this.isUs || !this.user.hide_followers
|
||||||
},
|
},
|
||||||
|
favoritesTabVisible () {
|
||||||
|
return this.isUs || !this.user.hide_favorites
|
||||||
|
},
|
||||||
currentUser () {
|
currentUser () {
|
||||||
return this.$store.state.users.currentUser
|
return this.$store.state.users.currentUser
|
||||||
},
|
},
|
||||||
@@ -114,14 +117,14 @@ const UserProfile = {
|
|||||||
replies: 'replies',
|
replies: 'replies',
|
||||||
media: 'media'
|
media: 'media'
|
||||||
}
|
}
|
||||||
// only we can see our own favourites
|
|
||||||
if (this.isUs) timelineTabMap['favorites'] = 'favorites'
|
if (this.favoritesTabVisible) timelineTabMap['favorites'] = 'favorites'
|
||||||
|
|
||||||
const timeline = timelineTabMap[nextTab]
|
const timeline = timelineTabMap[nextTab]
|
||||||
|
|
||||||
if (timeline) {
|
if (timeline) {
|
||||||
this.stopFetching()
|
this.stopFetching()
|
||||||
this.$store.dispatch('startFetchingTimeline', { timeline: timeline, userId: this.userId })
|
this.$store.dispatch('startFetchingTimeline', { timeline: timeline, userId: nextTab == 'favorites' && this.isUs ? null : this.userId })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
load (userNameOrId) {
|
load (userNameOrId) {
|
||||||
@@ -145,10 +148,12 @@ const UserProfile = {
|
|||||||
if (user) {
|
if (user) {
|
||||||
loadById(user.id)
|
loadById(user.id)
|
||||||
this.note = user.relationship.note
|
this.note = user.relationship.note
|
||||||
|
this.$store.dispatch('setDisplayBackground', user.background_image)
|
||||||
} else {
|
} else {
|
||||||
this.$store.dispatch('fetchUser', userNameOrId)
|
this.$store.dispatch('fetchUser', userNameOrId)
|
||||||
.then(({ id, relationship }) => {
|
.then(({ id, relationship, background_image }) => {
|
||||||
this.note = relationship.note
|
this.note = relationship.note
|
||||||
|
this.$store.dispatch('setDisplayBackground', background_image)
|
||||||
return loadById(id)
|
return loadById(id)
|
||||||
})
|
})
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
@@ -225,6 +230,9 @@ const UserProfile = {
|
|||||||
Conversation,
|
Conversation,
|
||||||
RichContent,
|
RichContent,
|
||||||
FollowedTagList
|
FollowedTagList
|
||||||
|
},
|
||||||
|
beforeRouteLeave(to, from) {
|
||||||
|
this.$store.dispatch('setDisplayBackground', null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,14 +165,14 @@
|
|||||||
:footer-slipgate="footerRef"
|
:footer-slipgate="footerRef"
|
||||||
/>
|
/>
|
||||||
<Timeline
|
<Timeline
|
||||||
v-if="isUs"
|
v-if="favoritesTabVisible"
|
||||||
key="favorites"
|
key="favorites"
|
||||||
:label="$t('user_card.favorites')"
|
:label="$t('user_card.favorites')"
|
||||||
:disabled="!isUs"
|
|
||||||
:embedded="true"
|
:embedded="true"
|
||||||
:title="$t('user_card.favorites')"
|
:title="$t('user_card.favorites')"
|
||||||
timeline-name="favorites"
|
timeline-name="favorites"
|
||||||
:timeline="favorites"
|
:timeline="favorites"
|
||||||
|
:user-id="isUs ? undefined : userId"
|
||||||
:in-profile="true"
|
:in-profile="true"
|
||||||
:footer-slipgate="footerRef"
|
:footer-slipgate="footerRef"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -884,7 +884,6 @@
|
|||||||
"upload_a_photo": "Pujar una foto",
|
"upload_a_photo": "Pujar una foto",
|
||||||
"useStreamingApi": "Rebre apunts i notificacions en temps real",
|
"useStreamingApi": "Rebre apunts i notificacions en temps real",
|
||||||
"useStreamingApiWarning": "És genial emprar-lo. Si es trenca, refresca, suposo?",
|
"useStreamingApiWarning": "És genial emprar-lo. Si es trenca, refresca, suposo?",
|
||||||
"use_at_icon": "Mostra el símbol {'@'} com a icona enlloc de text",
|
|
||||||
"use_contain_fit": "No retallar els adjunts en miniatures",
|
"use_contain_fit": "No retallar els adjunts en miniatures",
|
||||||
"use_one_click_nsfw": "Obre els adjunts NSFW amb només un clic",
|
"use_one_click_nsfw": "Obre els adjunts NSFW amb només un clic",
|
||||||
"user_mutes": "Usuaris",
|
"user_mutes": "Usuaris",
|
||||||
|
|||||||
@@ -916,7 +916,6 @@
|
|||||||
"upload_a_photo": "Lade ein Foto hoch",
|
"upload_a_photo": "Lade ein Foto hoch",
|
||||||
"useStreamingApi": "Empfange Posts und Benachrichtigungen in Echtzeit",
|
"useStreamingApi": "Empfange Posts und Benachrichtigungen in Echtzeit",
|
||||||
"useStreamingApiWarning": "(Nicht empfohlen, experimentell, bekannt dafür, Posts zu überspringen)",
|
"useStreamingApiWarning": "(Nicht empfohlen, experimentell, bekannt dafür, Posts zu überspringen)",
|
||||||
"use_at_icon": "{'@'}-Symbol als Icon und nicht als Text anzeigen",
|
|
||||||
"use_blurhash": "Blurhash für NSFW-Vorschauen verwenden",
|
"use_blurhash": "Blurhash für NSFW-Vorschauen verwenden",
|
||||||
"use_contain_fit": "Vorschaubilder nicht zuschneiden",
|
"use_contain_fit": "Vorschaubilder nicht zuschneiden",
|
||||||
"use_one_click_nsfw": "Heikle Anhänge mit nur einem Klick öffnen",
|
"use_one_click_nsfw": "Heikle Anhänge mit nur einem Klick öffnen",
|
||||||
|
|||||||
+3
-1
@@ -380,6 +380,7 @@
|
|||||||
"text/x.misskeymarkdown": "MFM"
|
"text/x.misskeymarkdown": "MFM"
|
||||||
},
|
},
|
||||||
"content_warning": "Content Warning (optional)",
|
"content_warning": "Content Warning (optional)",
|
||||||
|
"toggle_content_warning": "Toggle content warning",
|
||||||
"default": "Just arrived at Luna Nova Academy",
|
"default": "Just arrived at Luna Nova Academy",
|
||||||
"direct_warning_to_all": "This post will be visible to all the mentioned users.",
|
"direct_warning_to_all": "This post will be visible to all the mentioned users.",
|
||||||
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
|
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
|
||||||
@@ -600,6 +601,7 @@
|
|||||||
"list_aliases_error": "Error fetching aliases: {error}",
|
"list_aliases_error": "Error fetching aliases: {error}",
|
||||||
"list_backups_error": "Error fetching backup list: {error}",
|
"list_backups_error": "Error fetching backup list: {error}",
|
||||||
"lock_account_description": "Restrict your account to approved followers only",
|
"lock_account_description": "Restrict your account to approved followers only",
|
||||||
|
"permit_followback_description": "Automatically approve requests from already followed users",
|
||||||
"loop_video": "Loop videos",
|
"loop_video": "Loop videos",
|
||||||
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
|
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
|
||||||
"mascot": "Mastodon FE Mascot",
|
"mascot": "Mastodon FE Mascot",
|
||||||
@@ -749,6 +751,7 @@
|
|||||||
"show_nav_shortcuts": "Show extra navigation shortcuts in top panel",
|
"show_nav_shortcuts": "Show extra navigation shortcuts in top panel",
|
||||||
"show_panel_nav_shortcuts": "Show timeline navigation shortcuts at the top of the panel",
|
"show_panel_nav_shortcuts": "Show timeline navigation shortcuts at the top of the panel",
|
||||||
"show_scrollbars": "Show side column's scrollbars",
|
"show_scrollbars": "Show side column's scrollbars",
|
||||||
|
"show_page_backgrounds": "Show page-specific backgrounds, e.g. for user profiles",
|
||||||
"show_wider_shortcuts": "Show wider gap between top panel shortcuts",
|
"show_wider_shortcuts": "Show wider gap between top panel shortcuts",
|
||||||
"show_yous": "Show (You)s",
|
"show_yous": "Show (You)s",
|
||||||
"stop_gifs": "Pause animated images until you hover on them",
|
"stop_gifs": "Pause animated images until you hover on them",
|
||||||
@@ -922,7 +925,6 @@
|
|||||||
"upload_a_photo": "Upload a photo",
|
"upload_a_photo": "Upload a photo",
|
||||||
"useStreamingApi": "Receive posts and notifications real-time",
|
"useStreamingApi": "Receive posts and notifications real-time",
|
||||||
"useStreamingApiWarning": "It's cool use it. If it breaks refresh I guess?",
|
"useStreamingApiWarning": "It's cool use it. If it breaks refresh I guess?",
|
||||||
"use_at_icon": "Display {'@'} symbol as an icon instead of text",
|
|
||||||
"use_contain_fit": "Don't crop the attachment in thumbnails",
|
"use_contain_fit": "Don't crop the attachment in thumbnails",
|
||||||
"use_one_click_nsfw": "Open NSFW attachments with just one click",
|
"use_one_click_nsfw": "Open NSFW attachments with just one click",
|
||||||
"user_mutes": "Users",
|
"user_mutes": "Users",
|
||||||
|
|||||||
@@ -920,7 +920,6 @@
|
|||||||
"upload_a_photo": "Envoyer une photo",
|
"upload_a_photo": "Envoyer une photo",
|
||||||
"useStreamingApi": "Recevoir les messages et notifications en temps réel",
|
"useStreamingApi": "Recevoir les messages et notifications en temps réel",
|
||||||
"useStreamingApiWarning": "(Non recommandé, expérimental, connu pour rater des messages)",
|
"useStreamingApiWarning": "(Non recommandé, expérimental, connu pour rater des messages)",
|
||||||
"use_at_icon": "Afficher le symbol {'@'} comme une image",
|
|
||||||
"use_contain_fit": "Ne pas rogner les miniatures des pièces-jointes",
|
"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",
|
"use_one_click_nsfw": "Ouvrir les pièces-jointes sensibles avec un seul clic",
|
||||||
"user_mutes": "Comptes",
|
"user_mutes": "Comptes",
|
||||||
|
|||||||
@@ -921,7 +921,6 @@
|
|||||||
"upload_a_photo": "画像をアップロード",
|
"upload_a_photo": "画像をアップロード",
|
||||||
"useStreamingApi": "投稿と通知を、すぐに受け取る",
|
"useStreamingApi": "投稿と通知を、すぐに受け取る",
|
||||||
"useStreamingApiWarning": "(実験中で、投稿を取りこぼすかもしれないので、おすすめしません)",
|
"useStreamingApiWarning": "(実験中で、投稿を取りこぼすかもしれないので、おすすめしません)",
|
||||||
"use_at_icon": "{'@'}マークをアイコンにする",
|
|
||||||
"use_contain_fit": "画像のサムネイルを、切り抜かない",
|
"use_contain_fit": "画像のサムネイルを、切り抜かない",
|
||||||
"use_one_click_nsfw": "NSFWなファイルを1クリックで開く",
|
"use_one_click_nsfw": "NSFWなファイルを1クリックで開く",
|
||||||
"user_mutes": "ユーザー",
|
"user_mutes": "ユーザー",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const loaders = {
|
|||||||
ga: () => import('./ga.json'),
|
ga: () => import('./ga.json'),
|
||||||
he: () => import('./he.json'),
|
he: () => import('./he.json'),
|
||||||
hu: () => import('./hu.json'),
|
hu: () => import('./hu.json'),
|
||||||
|
id: () => import('./id.json'),
|
||||||
it: () => import('./it.json'),
|
it: () => import('./it.json'),
|
||||||
ja: () => import('./ja_pedantic.json'),
|
ja: () => import('./ja_pedantic.json'),
|
||||||
ja_easy: () => import('./ja_easy.json'),
|
ja_easy: () => import('./ja_easy.json'),
|
||||||
@@ -35,6 +36,7 @@ const loaders = {
|
|||||||
sk: () => import('./sk.json'),
|
sk: () => import('./sk.json'),
|
||||||
te: () => import('./te.json'),
|
te: () => import('./te.json'),
|
||||||
uk: () => import('./uk.json'),
|
uk: () => import('./uk.json'),
|
||||||
|
vi: () => import('./vi.json'),
|
||||||
zh: () => import('./zh.json'),
|
zh: () => import('./zh.json'),
|
||||||
zh_Hant: () => import('./zh_Hant.json')
|
zh_Hant: () => import('./zh_Hant.json')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -918,7 +918,6 @@
|
|||||||
"upload_a_photo": "Foto uploaden",
|
"upload_a_photo": "Foto uploaden",
|
||||||
"useStreamingApi": "Berichten en meldingen in real-time ontvangen",
|
"useStreamingApi": "Berichten en meldingen in real-time ontvangen",
|
||||||
"useStreamingApiWarning": "Iets experimenteels met berichten streamen uwu miss kun je beter uit laten ofzo?",
|
"useStreamingApiWarning": "Iets experimenteels met berichten streamen uwu miss kun je beter uit laten ofzo?",
|
||||||
"use_at_icon": "{'@'} symbool als icoon tonen in plaats van tekst",
|
|
||||||
"use_blurhash": "Waas tonen over NSFW-miniaturen",
|
"use_blurhash": "Waas tonen over NSFW-miniaturen",
|
||||||
"use_contain_fit": "Bijlage in miniaturen niet bijsnijden",
|
"use_contain_fit": "Bijlage in miniaturen niet bijsnijden",
|
||||||
"use_one_click_nsfw": "Gevoelige bijlagen met slechts één klik openen",
|
"use_one_click_nsfw": "Gevoelige bijlagen met slechts één klik openen",
|
||||||
|
|||||||
@@ -922,7 +922,6 @@
|
|||||||
"upload_a_photo": "Вивантажити фото",
|
"upload_a_photo": "Вивантажити фото",
|
||||||
"useStreamingApi": "Отримувати дописи та сповіщення наживо",
|
"useStreamingApi": "Отримувати дописи та сповіщення наживо",
|
||||||
"useStreamingApiWarning": "Загалом працює. Якщо не зовсім, спробуєте оновити сторінку?",
|
"useStreamingApiWarning": "Загалом працює. Якщо не зовсім, спробуєте оновити сторінку?",
|
||||||
"use_at_icon": "Значок {'@'} замість символу",
|
|
||||||
"use_blurhash": "Показувати дражливі мініатюри (як розмиті кольори)",
|
"use_blurhash": "Показувати дражливі мініатюри (як розмиті кольори)",
|
||||||
"use_contain_fit": "Не обрізати краї мініатюр",
|
"use_contain_fit": "Не обрізати краї мініатюр",
|
||||||
"use_one_click_nsfw": "Відкривати NSFW вкладення одним кліком миші",
|
"use_one_click_nsfw": "Відкривати NSFW вкладення одним кліком миші",
|
||||||
|
|||||||
@@ -922,7 +922,6 @@
|
|||||||
"upload_a_photo": "上传照片",
|
"upload_a_photo": "上传照片",
|
||||||
"useStreamingApi": "实时接收帖文和通知",
|
"useStreamingApi": "实时接收帖文和通知",
|
||||||
"useStreamingApiWarning": "十分炫酷推荐使用。要是崩了试试刷新?",
|
"useStreamingApiWarning": "十分炫酷推荐使用。要是崩了试试刷新?",
|
||||||
"use_at_icon": "将 {'@'} 符号显示为图标而不是文本",
|
|
||||||
"use_blurhash": "对NSFW的缩略图使用模糊处理",
|
"use_blurhash": "对NSFW的缩略图使用模糊处理",
|
||||||
"use_contain_fit": "生成缩略图时不要裁剪附件",
|
"use_contain_fit": "生成缩略图时不要裁剪附件",
|
||||||
"use_one_click_nsfw": "点击一次以打开工作场所不适宜(NSFW)的附件",
|
"use_one_click_nsfw": "点击一次以打开工作场所不适宜(NSFW)的附件",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export const defaultState = {
|
|||||||
alwaysShowNewPostButton: false,
|
alwaysShowNewPostButton: false,
|
||||||
autohideFloatingPostButton: false,
|
autohideFloatingPostButton: false,
|
||||||
pauseOnUnfocused: true,
|
pauseOnUnfocused: true,
|
||||||
|
displayPageBackgrounds: true,
|
||||||
stopGifs: undefined,
|
stopGifs: undefined,
|
||||||
replyVisibility: 'all',
|
replyVisibility: 'all',
|
||||||
thirdColumnMode: 'notifications',
|
thirdColumnMode: 'notifications',
|
||||||
@@ -95,7 +96,6 @@ export const defaultState = {
|
|||||||
disableStickyHeaders: false,
|
disableStickyHeaders: false,
|
||||||
showScrollbars: false,
|
showScrollbars: false,
|
||||||
greentext: undefined, // instance default
|
greentext: undefined, // instance default
|
||||||
useAtIcon: undefined, // instance default
|
|
||||||
mentionLinkDisplay: undefined, // instance default
|
mentionLinkDisplay: undefined, // instance default
|
||||||
mentionLinkShowTooltip: undefined, // instance default
|
mentionLinkShowTooltip: undefined, // instance default
|
||||||
mentionLinkShowAvatar: undefined, // instance default
|
mentionLinkShowAvatar: undefined, // instance default
|
||||||
@@ -229,7 +229,7 @@ const config = {
|
|||||||
break
|
break
|
||||||
case 'interfaceLanguage':
|
case 'interfaceLanguage':
|
||||||
messages.setLanguage(this.getters.i18n, value)
|
messages.setLanguage(this.getters.i18n, value)
|
||||||
Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
|
Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value), {sameSite: 'Lax'})
|
||||||
dispatch('setInstanceOption', { name: 'interfaceLanguage', value })
|
dispatch('setInstanceOption', { name: 'interfaceLanguage', value })
|
||||||
break
|
break
|
||||||
case 'thirdColumnMode':
|
case 'thirdColumnMode':
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const defaultState = {
|
|||||||
background: '/static/aurora_borealis.jpg',
|
background: '/static/aurora_borealis.jpg',
|
||||||
collapseMessageWithSubject: true,
|
collapseMessageWithSubject: true,
|
||||||
greentext: false,
|
greentext: false,
|
||||||
useAtIcon: false,
|
|
||||||
mentionLinkDisplay: 'short',
|
mentionLinkDisplay: 'short',
|
||||||
mentionLinkShowTooltip: true,
|
mentionLinkShowTooltip: true,
|
||||||
mentionLinkShowAvatar: false,
|
mentionLinkShowAvatar: false,
|
||||||
|
|||||||
@@ -37,11 +37,18 @@ const recentEmojis = {
|
|||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
recentEmojis: (state, getters, rootState) => state.emojis.reduce((objects, displayText) => {
|
recentEmojis: (state, getters, rootState) => state.emojis.reduce((objects, displayText) => {
|
||||||
const allEmojis = rootState.instance.emoji.concat(rootState.instance.customEmoji)
|
let comparator = emoji => emoji.displayText === displayText
|
||||||
let emojiObject = allEmojis.find(emoji => emoji.displayText === displayText)
|
|
||||||
|
let emojiObject = rootState.instance.emoji.find(comparator)
|
||||||
|
if (emojiObject !== undefined) {
|
||||||
|
objects.push(emojiObject)
|
||||||
|
} else {
|
||||||
|
emojiObject = rootState.instance.customEmoji.find(comparator)
|
||||||
if (emojiObject !== undefined) {
|
if (emojiObject !== undefined) {
|
||||||
objects.push(emojiObject)
|
objects.push(emojiObject)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return objects
|
return objects
|
||||||
}, []),
|
}, []),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ export const settingsMap = {
|
|||||||
},
|
},
|
||||||
// Privacy
|
// Privacy
|
||||||
'locked': 'locked',
|
'locked': 'locked',
|
||||||
|
'permitFollowback': {
|
||||||
|
get: 'akkoma.permit_followback',
|
||||||
|
set: 'permit_followback'
|
||||||
|
},
|
||||||
'allowFollowingMove': {
|
'allowFollowingMove': {
|
||||||
get: 'pleroma.allow_following_move',
|
get: 'pleroma.allow_following_move',
|
||||||
set: 'allow_following_move'
|
set: 'allow_following_move'
|
||||||
|
|||||||
@@ -135,6 +135,10 @@ export const mutations = {
|
|||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
user['deactivated'] = deactivated
|
user['deactivated'] = deactivated
|
||||||
},
|
},
|
||||||
|
setDisplayBackground(state, url) {
|
||||||
|
console.log("Commiting user profile bg mutation")
|
||||||
|
state.displayBackground = url
|
||||||
|
},
|
||||||
setCurrentUser (state, user) {
|
setCurrentUser (state, user) {
|
||||||
state.lastLoginName = user.screen_name
|
state.lastLoginName = user.screen_name
|
||||||
state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength)
|
state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength)
|
||||||
@@ -307,6 +311,7 @@ export const defaultState = {
|
|||||||
currentUser: false,
|
currentUser: false,
|
||||||
users: [],
|
users: [],
|
||||||
usersObject: {},
|
usersObject: {},
|
||||||
|
displayBackground: null,
|
||||||
signUpPending: false,
|
signUpPending: false,
|
||||||
signUpErrors: [],
|
signUpErrors: [],
|
||||||
relationships: {},
|
relationships: {},
|
||||||
@@ -319,6 +324,10 @@ const users = {
|
|||||||
mutations,
|
mutations,
|
||||||
getters,
|
getters,
|
||||||
actions: {
|
actions: {
|
||||||
|
setDisplayBackground (store, url) {
|
||||||
|
console.log("Performing user profile bg action...")
|
||||||
|
store.commit('setDisplayBackground', url)
|
||||||
|
},
|
||||||
fetchUserIfMissing (store, id) {
|
fetchUserIfMissing (store, id) {
|
||||||
if (!store.getters.findUser(id)) {
|
if (!store.getters.findUser(id)) {
|
||||||
store.dispatch('fetchUser', id)
|
store.dispatch('fetchUser', id)
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
|
|||||||
const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
|
const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
|
||||||
const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
|
const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
|
||||||
const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
|
const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
|
||||||
|
const PLEROMA_USER_FAVORITES_TIMELINE_URL = id => `/api/v1/pleroma/accounts/${id}/favourites`
|
||||||
const AKKOMA_SETTING_PROFILE_URL = (name) => `/api/v1/akkoma/frontend_settings/pleroma-fe/${name}`
|
const AKKOMA_SETTING_PROFILE_URL = (name) => `/api/v1/akkoma/frontend_settings/pleroma-fe/${name}`
|
||||||
const AKKOMA_SETTING_PROFILE_LIST = `/api/v1/akkoma/frontend_settings/pleroma-fe`
|
const AKKOMA_SETTING_PROFILE_LIST = `/api/v1/akkoma/frontend_settings/pleroma-fe`
|
||||||
const MASTODON_TAG_URL = (name) => `/api/v1/tags/${name}`
|
const MASTODON_TAG_URL = (name) => `/api/v1/tags/${name}`
|
||||||
@@ -709,6 +710,7 @@ const fetchTimeline = ({
|
|||||||
media: MASTODON_USER_TIMELINE_URL,
|
media: MASTODON_USER_TIMELINE_URL,
|
||||||
list: MASTODON_LIST_TIMELINE_URL,
|
list: MASTODON_LIST_TIMELINE_URL,
|
||||||
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
|
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
|
||||||
|
publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL,
|
||||||
tag: MASTODON_TAG_TIMELINE_URL,
|
tag: MASTODON_TAG_TIMELINE_URL,
|
||||||
bookmarks: MASTODON_BOOKMARK_TIMELINE_URL
|
bookmarks: MASTODON_BOOKMARK_TIMELINE_URL
|
||||||
}
|
}
|
||||||
@@ -717,6 +719,10 @@ const fetchTimeline = ({
|
|||||||
|
|
||||||
let url = timelineUrls[timeline]
|
let url = timelineUrls[timeline]
|
||||||
|
|
||||||
|
if (timeline === 'favorites' && userId) {
|
||||||
|
url = timelineUrls.publicFavorites(userId)
|
||||||
|
}
|
||||||
|
|
||||||
if (timeline === 'user' || timeline === 'media' || timeline === 'replies') {
|
if (timeline === 'user' || timeline === 'media' || timeline === 'replies') {
|
||||||
url = url(userId)
|
url = url(userId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export const parseUser = (data) => {
|
|||||||
if (data.akkoma) {
|
if (data.akkoma) {
|
||||||
output.instance = data.akkoma.instance
|
output.instance = data.akkoma.instance
|
||||||
output.status_ttl_days = data.akkoma.status_ttl_days
|
output.status_ttl_days = data.akkoma.status_ttl_days
|
||||||
|
output.permit_followback = data.akkoma.permit_followback
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.pleroma) {
|
if (data.pleroma) {
|
||||||
@@ -109,6 +110,7 @@ export const parseUser = (data) => {
|
|||||||
|
|
||||||
output.allow_following_move = data.pleroma.allow_following_move
|
output.allow_following_move = data.pleroma.allow_following_move
|
||||||
|
|
||||||
|
output.hide_favorites = data.pleroma.hide_favorites
|
||||||
output.hide_follows = data.pleroma.hide_follows
|
output.hide_follows = data.pleroma.hide_follows
|
||||||
output.hide_followers = data.pleroma.hide_followers
|
output.hide_followers = data.pleroma.hide_followers
|
||||||
output.hide_follows_count = data.pleroma.hide_follows_count
|
output.hide_follows_count = data.pleroma.hide_follows_count
|
||||||
|
|||||||
@@ -8,7 +8,30 @@ const specialLanguageCodes = {
|
|||||||
'zh': 'zh-Hans'
|
'zh': 'zh-Hans'
|
||||||
}
|
}
|
||||||
|
|
||||||
const internalToBrowserLocale = code => specialLanguageCodes[code] || code
|
// Find a browser language that matches the configured UI language.
|
||||||
|
// Browser language should match the configured generic short code prefix:
|
||||||
|
// eg 'en-GB' browser language matches 'en' UI language.
|
||||||
|
const findBrowserRegionMatch = genericLang => {
|
||||||
|
for (const blang of window.navigator.languages) {
|
||||||
|
if (genericLang === blang.split('-')[0])
|
||||||
|
return blang;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const internalToBrowserLocale = (() => {
|
||||||
|
const resolvedBrowserLocales = {}
|
||||||
|
|
||||||
|
return i18nLocale => {
|
||||||
|
if (resolvedBrowserLocales[i18nLocale]) {
|
||||||
|
return resolvedBrowserLocales[i18nLocale]
|
||||||
|
}
|
||||||
|
const lang = specialLanguageCodes[i18nLocale] || i18nLocale;
|
||||||
|
const resolved = findBrowserRegionMatch(lang) || lang;
|
||||||
|
resolvedBrowserLocales[i18nLocale] = resolved
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
|
||||||
const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-')
|
const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user