forked from AkkomaGang/akkoma-fe
Compare commits
46 commits
Author | SHA1 | Date | |
---|---|---|---|
floatingghost | 3e86db24d3 | ||
floatingghost | 7789c5def6 | ||
Floatingghost | f3934afbd8 | ||
Floatingghost | 0b437ab6fd | ||
floatingghost | 8964dce609 | ||
floatingghost | 1a49a1b3ac | ||
Floatingghost | 1adef56603 | ||
Floatingghost | 5aaa605df0 | ||
Floatingghost | 71e287d56c | ||
floatingghost | 70275684bf | ||
floatingghost | 29ff2ef455 | ||
Oneric | 62e0dd858c | ||
sarah | cc709394c5 | ||
Serhii Tereshchenko | 57beea6a0d | ||
Serhii Tereshchenko | 042e8c78dc | ||
Oneric | 0e07d88afa | ||
sarah | 1f5f8665c8 | ||
Oneric | 428ed70b0d | ||
floatingghost | ed0b403c33 | ||
floatingghost | 0f842b300b | ||
floatingghost | 865cb6f96a | ||
Oneric | 050c7df2e6 | ||
Oneric | a77a9e04d9 | ||
leap123 | a57334991e | ||
floatingghost | 8dce31d0ad | ||
Hazel Koehler | ea9ad4d600 | ||
Hazel Koehler | 34e2800f59 | ||
Hazel Koehler | 3d65eccf04 | ||
floatingghost | d304be654f | ||
floatingghost | aee97fa948 | ||
floatingghost | 7da1687f31 | ||
floatingghost | a8f193d4bd | ||
floatingghost | 81c82e11bc | ||
floatingghost | 00cadce5b4 | ||
floatingghost | 40a08f279b | ||
Oneric | c524a47e6f | ||
Ngô Ngọc Đức Huy | 235c734d37 | ||
Ngô Ngọc Đức Huy | deaef1d0b9 | ||
smitten | 1b28ec3b72 | ||
smitten | c9dc8f00f9 | ||
Oneric | beee99e733 | ||
Oneric | ccb0ffdc8a | ||
Oneric | ab250c2f3a | ||
Norm | 1de62fffcd | ||
Norm | 306cea04a1 | ||
Mergan | d9e1bc4d99 |
1
.tool-versions
Normal file
1
.tool-versions
Normal file
|
@ -0,0 +1 @@
|
|||
nodejs 20.12.2
|
|
@ -1,10 +1,12 @@
|
|||
platform: linux/amd64
|
||||
pipeline:
|
||||
labels:
|
||||
platform: linux/amd64
|
||||
|
||||
steps:
|
||||
lint:
|
||||
when:
|
||||
event:
|
||||
- pull_request
|
||||
image: node:18
|
||||
image: node:20
|
||||
commands:
|
||||
- yarn
|
||||
- yarn lint
|
||||
|
@ -14,7 +16,7 @@ pipeline:
|
|||
when:
|
||||
event:
|
||||
- pull_request
|
||||
image: node:18
|
||||
image: node:20
|
||||
commands:
|
||||
- apt update
|
||||
- apt install firefox-esr -y --no-install-recommends
|
||||
|
@ -28,7 +30,7 @@ pipeline:
|
|||
branch:
|
||||
- develop
|
||||
- stable
|
||||
image: node:18
|
||||
image: node:20
|
||||
commands:
|
||||
- yarn
|
||||
- yarn build
|
||||
|
@ -40,15 +42,15 @@ pipeline:
|
|||
branch:
|
||||
- develop
|
||||
- stable
|
||||
image: node:18
|
||||
image: node:20
|
||||
secrets:
|
||||
- SCW_ACCESS_KEY
|
||||
- SCW_SECRET_KEY
|
||||
- SCW_DEFAULT_ORGANIZATION_ID
|
||||
commands:
|
||||
- apt-get update && apt-get install -y rclone wget zip
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
|
||||
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64
|
||||
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli
|
||||
- chmod +x scaleway-cli
|
||||
- ./scaleway-cli object config install type=rclone
|
||||
- zip akkoma-fe.zip -r dist
|
||||
|
@ -70,8 +72,8 @@ pipeline:
|
|||
- 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.5.1/scaleway-cli_2.5.1_linux_amd64
|
||||
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
|
||||
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64
|
||||
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli
|
||||
- chmod +x scaleway-cli
|
||||
- ./scaleway-cli object config install type=rclone
|
||||
- cd docs
|
||||
|
|
|
@ -22,7 +22,7 @@ To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/ad
|
|||
|
||||
``` bash
|
||||
# install dependencies
|
||||
npm install -g yarn
|
||||
corepack enable
|
||||
yarn
|
||||
|
||||
# serve with hot reload at localhost:8080
|
||||
|
@ -37,7 +37,7 @@ npm run unit
|
|||
|
||||
# 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.
|
||||
* `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
|
||||
}
|
||||
|
|
|
@ -64,6 +64,11 @@ export default {
|
|||
'-' + this.layoutType
|
||||
]
|
||||
},
|
||||
pageBackground () {
|
||||
return this.mergedConfig.displayPageBackgrounds
|
||||
? this.$store.state.users.displayBackground
|
||||
: null
|
||||
},
|
||||
currentUser () { return this.$store.state.users.currentUser },
|
||||
userBackground () { return this.currentUser.background_image },
|
||||
instanceBackground () {
|
||||
|
@ -71,7 +76,7 @@ export default {
|
|||
? null
|
||||
: this.$store.state.instance.background
|
||||
},
|
||||
background () { return this.userBackground || this.instanceBackground },
|
||||
background () { return this.pageBackground || this.userBackground || this.instanceBackground },
|
||||
bgStyle () {
|
||||
if (this.background) {
|
||||
return {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
}
|
||||
|
||||
html {
|
||||
font-size: 14px;
|
||||
font-size: 0.875rem;
|
||||
// overflow-x: clip causes my browser's tab to crash with SIGILL lul
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
text-overflow: ellipsis;
|
||||
overflow: scroll;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&.-static {
|
||||
|
|
|
@ -93,9 +93,6 @@ const MentionLink = {
|
|||
this.highlightType
|
||||
]
|
||||
},
|
||||
useAtIcon () {
|
||||
return this.mergedConfig.useAtIcon
|
||||
},
|
||||
isRemote () {
|
||||
return this.userName !== this.userNameFull
|
||||
},
|
||||
|
|
|
@ -6,6 +6,7 @@ import UserCard from '../user_card/user_card.vue'
|
|||
import Timeago from '../timeago/timeago.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
@ -50,7 +51,8 @@ const Notification = {
|
|||
Timeago,
|
||||
Status,
|
||||
RichContent,
|
||||
ConfirmModal
|
||||
ConfirmModal,
|
||||
StillImage
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
|
|
|
@ -116,12 +116,13 @@
|
|||
scope="global"
|
||||
keypath="notifications.reacted_with"
|
||||
>
|
||||
<img
|
||||
<still-image
|
||||
v-if="notification.emoji_url !== null"
|
||||
class="notification-reaction-emoji"
|
||||
:src="notification.emoji_url"
|
||||
:name="notification.emoji"
|
||||
>
|
||||
:title="notification.emoji"
|
||||
:alt="notification.emoji"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="emoji-reaction-emoji"
|
||||
|
@ -151,7 +152,6 @@
|
|||
>
|
||||
<Timeago
|
||||
:time="notification.created_at"
|
||||
:with-direction="true"
|
||||
:auto-update="240"
|
||||
/>
|
||||
</router-link>
|
||||
|
|
|
@ -105,9 +105,12 @@
|
|||
flex: 1;
|
||||
padding-left: 0.8em;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.heading-right, .notification-right {
|
||||
.timeago {
|
||||
min-width: 3em;
|
||||
display: inline-block;
|
||||
min-width: 6em;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,11 +9,12 @@ import StatusContent from '../status_content/status_content.vue'
|
|||
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 suggestor from '../emoji_input/suggestor.js'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
import Select from '../select/select.vue'
|
||||
import iso6391 from 'iso-639-1'
|
||||
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
@ -62,6 +63,13 @@ const deleteDraft = (draftKey) => {
|
|||
localStorage.setItem('drafts', JSON.stringify(draftData));
|
||||
}
|
||||
|
||||
const interfaceToISOLanguage = (ilang) => {
|
||||
const sep = ilang.indexOf("_");
|
||||
return sep < 0 ?
|
||||
ilang :
|
||||
ilang.substr(0, sep);
|
||||
}
|
||||
|
||||
const PostStatusForm = {
|
||||
props: [
|
||||
'statusId',
|
||||
|
@ -129,6 +137,13 @@ const PostStatusForm = {
|
|||
this.$refs.textarea.focus()
|
||||
}
|
||||
},
|
||||
setup() {
|
||||
const {postLanguageOptions} = usePostLanguageOptions()
|
||||
|
||||
return {
|
||||
postLanguageOptions,
|
||||
}
|
||||
},
|
||||
data () {
|
||||
const preset = this.$route.query.message
|
||||
let statusText = preset || ''
|
||||
|
@ -138,7 +153,8 @@ const PostStatusForm = {
|
|||
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
||||
}
|
||||
|
||||
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage } = this.$store.getters.mergedConfig
|
||||
const { postContentType: contentType, postLanguage: defaultPostLanguage, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage, alwaysShowSubjectInput } = this.$store.getters.mergedConfig
|
||||
const postLanguage = defaultPostLanguage || interfaceToISOLanguage(interfaceLanguage)
|
||||
|
||||
let statusParams = {
|
||||
spoilerText: this.subject || '',
|
||||
|
@ -149,7 +165,7 @@ const PostStatusForm = {
|
|||
poll: {},
|
||||
mediaDescriptions: {},
|
||||
visibility: this.suggestedVisibility(),
|
||||
language: interfaceLanguage,
|
||||
language: postLanguage,
|
||||
contentType
|
||||
}
|
||||
|
||||
|
@ -164,7 +180,7 @@ const PostStatusForm = {
|
|||
poll: this.statusPoll || {},
|
||||
mediaDescriptions: this.statusMediaDescriptions || {},
|
||||
visibility: this.statusScope || this.suggestedVisibility(),
|
||||
language: this.statusLanguage || interfaceLanguage,
|
||||
language: this.statusLanguage || postLanguage,
|
||||
contentType: statusContentType
|
||||
}
|
||||
}
|
||||
|
@ -199,6 +215,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 {
|
||||
dropFiles: [],
|
||||
uploadingFiles: false,
|
||||
|
@ -213,7 +233,10 @@ const PostStatusForm = {
|
|||
preview: null,
|
||||
previewLoading: false,
|
||||
emojiInputShown: false,
|
||||
idempotencyKey: ''
|
||||
idempotencyKey: '',
|
||||
activeEmojiInput: undefined,
|
||||
activeTextInput: undefined,
|
||||
subjectVisible: showSubject
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -302,9 +325,6 @@ const PostStatusForm = {
|
|||
...mapState({
|
||||
mobileLayout: state => state.interface.mobileLayout
|
||||
}),
|
||||
isoLanguages () {
|
||||
return iso6391.getAllCodes();
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'newStatus': {
|
||||
|
@ -674,8 +694,33 @@ const PostStatusForm = {
|
|||
this.$refs['emoji-input'].resize()
|
||||
},
|
||||
showEmojiPicker () {
|
||||
this.$refs['textarea'].focus()
|
||||
this.$refs['emoji-input'].triggerShowPicker()
|
||||
if (!this.activeEmojiInput || !this.activeTextInput)
|
||||
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 () {
|
||||
this.error = null
|
||||
|
|
|
@ -118,13 +118,16 @@
|
|||
/>
|
||||
</div>
|
||||
<EmojiInput
|
||||
v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
|
||||
ref="subject-emoji-input"
|
||||
v-if="subjectVisible"
|
||||
v-model="newStatus.spoilerText"
|
||||
enable-emoji-picker
|
||||
hide-emoji-button
|
||||
:suggest="emojiSuggestor"
|
||||
class="form-control"
|
||||
>
|
||||
<input
|
||||
ref="subject-input"
|
||||
v-model="newStatus.spoilerText"
|
||||
type="text"
|
||||
:placeholder="$t('post_status.content_warning')"
|
||||
|
@ -132,6 +135,7 @@
|
|||
size="1"
|
||||
class="form-post-subject"
|
||||
@input="onSubjectInput"
|
||||
@focus="focusSubjectInput()"
|
||||
>
|
||||
</EmojiInput>
|
||||
<i18n-t
|
||||
|
@ -173,6 +177,7 @@
|
|||
@input="resize"
|
||||
@compositionupdate="resize"
|
||||
@paste="paste"
|
||||
@focus="focusStatusInput()"
|
||||
/>
|
||||
<p
|
||||
v-if="hasStatusLengthLimit"
|
||||
|
@ -203,11 +208,11 @@
|
|||
class="form-control"
|
||||
>
|
||||
<option
|
||||
v-for="language in isoLanguages"
|
||||
:key="language"
|
||||
:value="language"
|
||||
v-for="language in postLanguageOptions"
|
||||
:key="language.key"
|
||||
:value="language.value"
|
||||
>
|
||||
{{ language }}
|
||||
{{ language.label }}
|
||||
</option>
|
||||
</Select>
|
||||
</div>
|
||||
|
@ -276,6 +281,15 @@
|
|||
>
|
||||
<FAIcon icon="poll-h" />
|
||||
</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>
|
||||
<button
|
||||
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;
|
||||
line-height: 1.1;
|
||||
flex: 1;
|
||||
|
@ -499,6 +513,11 @@
|
|||
|
||||
.poll-icon {
|
||||
order: 3;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.spoiler-icon {
|
||||
order: 4;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
|
|||
import IntegerSetting from '../helpers/integer_setting.vue'
|
||||
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||
|
||||
import { usePostLanguageOptions } from 'src/lib/post_language'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import ServerSideIndicator from '../helpers/server_side_indicator.vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
@ -17,6 +18,11 @@ library.add(
|
|||
)
|
||||
|
||||
const GeneralTab = {
|
||||
setup() {
|
||||
const {postLanguageOptions} = usePostLanguageOptions()
|
||||
|
||||
return {postLanguageOptions}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({
|
||||
|
@ -118,6 +124,12 @@ const GeneralTab = {
|
|||
this.$store.dispatch('setOption', { name: 'translationLanguage', value: val })
|
||||
}
|
||||
},
|
||||
postLanguage: {
|
||||
get: function () { return this.$store.getters.mergedConfig.postLanguage },
|
||||
set: function (val) {
|
||||
this.$store.dispatch('setOption', { name: 'postLanguage', value: val })
|
||||
}
|
||||
},
|
||||
...SharedComputedObject()
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -146,6 +146,11 @@
|
|||
{{ $t('settings.show_wider_shortcuts') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="displayPageBackgrounds">
|
||||
{{ $t('settings.show_page_backgrounds') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="stopGifs">
|
||||
{{ $t('settings.stop_gifs') }}
|
||||
|
@ -483,14 +488,6 @@
|
|||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="useAtIcon"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.use_at_icon') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="mentionLinkShowAvatar">
|
||||
{{ $t('settings.mention_link_show_avatar') }}
|
||||
|
@ -588,6 +585,15 @@
|
|||
{{ $t('settings.post_status_content_type') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
<li>
|
||||
<ChoiceSetting
|
||||
id="postLanguage"
|
||||
path="postLanguage"
|
||||
:options="postLanguageOptions"
|
||||
>
|
||||
{{ $t('settings.post_language') }}
|
||||
</ChoiceSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="alwaysShowNewPostButton"
|
||||
|
|
|
@ -33,6 +33,7 @@ const ProfileTab = {
|
|||
newName: this.$store.state.users.currentUser.name_unescaped,
|
||||
newBio: unescape(this.$store.state.users.currentUser.description),
|
||||
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 })),
|
||||
showRole: this.$store.state.users.currentUser.show_role,
|
||||
role: this.$store.state.users.currentUser.role,
|
||||
|
@ -135,6 +136,7 @@ const ProfileTab = {
|
|||
bot: this.bot,
|
||||
show_role: this.showRole,
|
||||
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1,
|
||||
permit_followback: this.permit_followback,
|
||||
accepts_direct_messages_from: this.userAcceptsDirectMessagesFrom
|
||||
/* eslint-enable camelcase */
|
||||
}
|
||||
|
|
|
@ -259,6 +259,19 @@
|
|||
<BooleanSetting path="serverSide_locked">
|
||||
{{ $t('settings.lock_account_description') }}
|
||||
</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>
|
||||
<BooleanSetting path="serverSide_discoverable">
|
||||
|
|
|
@ -190,7 +190,7 @@
|
|||
>
|
||||
<Timeago
|
||||
:time="status.created_at"
|
||||
:with-direction="true"
|
||||
:with-direction="!compact"
|
||||
:auto-update="60"
|
||||
/>
|
||||
</router-link>
|
||||
|
|
|
@ -42,20 +42,22 @@ const StillImage = {
|
|||
detectAnimation (image) {
|
||||
const mediaProxyAvailable = this.$store.state.instance.mediaProxyAvailable
|
||||
|
||||
// harmless CORS errors without-- clean console with
|
||||
if (!mediaProxyAvailable) {
|
||||
// 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
|
||||
// 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) {
|
||||
// Only for no media-proxy for now, since we're capturing gif, webp, and apng (are there others?)
|
||||
// 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
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.detectWithoutMediaProxy(image)
|
||||
} else {
|
||||
this.detectWithMediaProxy(image)
|
||||
}
|
||||
},
|
||||
detectAnimationWithFetch (image) {
|
||||
// Browser Cache should ensure image doesn't get loaded twice if cache exists
|
||||
fetch(image.src, {
|
||||
referrerPolicy: 'same-origin'
|
||||
|
@ -85,6 +87,36 @@ const StillImage = {
|
|||
// 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;
|
||||
},
|
||||
|
|
|
@ -145,10 +145,12 @@ const UserProfile = {
|
|||
if (user) {
|
||||
loadById(user.id)
|
||||
this.note = user.relationship.note
|
||||
this.$store.dispatch('setDisplayBackground', user.background_image)
|
||||
} else {
|
||||
this.$store.dispatch('fetchUser', userNameOrId)
|
||||
.then(({ id, relationship }) => {
|
||||
.then(({ id, relationship, background_image }) => {
|
||||
this.note = relationship.note
|
||||
this.$store.dispatch('setDisplayBackground', background_image)
|
||||
return loadById(id)
|
||||
})
|
||||
.catch((reason) => {
|
||||
|
@ -225,6 +227,9 @@ const UserProfile = {
|
|||
Conversation,
|
||||
RichContent,
|
||||
FollowedTagList
|
||||
},
|
||||
beforeRouteLeave(to, from) {
|
||||
this.$store.dispatch('setDisplayBackground', null)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -884,7 +884,6 @@
|
|||
"upload_a_photo": "Pujar una foto",
|
||||
"useStreamingApi": "Rebre apunts i notificacions en temps real",
|
||||
"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_one_click_nsfw": "Obre els adjunts NSFW amb només un clic",
|
||||
"user_mutes": "Usuaris",
|
||||
|
|
|
@ -916,7 +916,6 @@
|
|||
"upload_a_photo": "Lade ein Foto hoch",
|
||||
"useStreamingApi": "Empfange Posts und Benachrichtigungen in Echtzeit",
|
||||
"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_contain_fit": "Vorschaubilder nicht zuschneiden",
|
||||
"use_one_click_nsfw": "Heikle Anhänge mit nur einem Klick öffnen",
|
||||
|
|
|
@ -380,6 +380,7 @@
|
|||
"text/x.misskeymarkdown": "MFM"
|
||||
},
|
||||
"content_warning": "Content Warning (optional)",
|
||||
"toggle_content_warning": "Toggle content warning",
|
||||
"default": "Just arrived at Luna Nova Academy",
|
||||
"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.",
|
||||
|
@ -600,6 +601,7 @@
|
|||
"list_aliases_error": "Error fetching aliases: {error}",
|
||||
"list_backups_error": "Error fetching backup list: {error}",
|
||||
"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_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
|
||||
"mascot": "Mastodon FE Mascot",
|
||||
|
@ -682,6 +684,7 @@
|
|||
"play_videos_in_modal": "Play videos in a popup frame",
|
||||
"post_look_feel": "Posts Look & Feel",
|
||||
"post_status_content_type": "Default post content type",
|
||||
"post_language": "Default post language",
|
||||
"posts": "Posts",
|
||||
"preload_images": "Preload images",
|
||||
"presets": "Presets",
|
||||
|
@ -749,6 +752,7 @@
|
|||
"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_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_yous": "Show (You)s",
|
||||
"stop_gifs": "Pause animated images until you hover on them",
|
||||
|
@ -922,7 +926,6 @@
|
|||
"upload_a_photo": "Upload a photo",
|
||||
"useStreamingApi": "Receive posts and notifications real-time",
|
||||
"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_one_click_nsfw": "Open NSFW attachments with just one click",
|
||||
"user_mutes": "Users",
|
||||
|
|
|
@ -920,7 +920,6 @@
|
|||
"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_at_icon": "Afficher le symbol {'@'} comme une image",
|
||||
"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_mutes": "Comptes",
|
||||
|
|
|
@ -921,7 +921,6 @@
|
|||
"upload_a_photo": "画像をアップロード",
|
||||
"useStreamingApi": "投稿と通知を、すぐに受け取る",
|
||||
"useStreamingApiWarning": "(実験中で、投稿を取りこぼすかもしれないので、おすすめしません)",
|
||||
"use_at_icon": "{'@'}マークをアイコンにする",
|
||||
"use_contain_fit": "画像のサムネイルを、切り抜かない",
|
||||
"use_one_click_nsfw": "NSFWなファイルを1クリックで開く",
|
||||
"user_mutes": "ユーザー",
|
||||
|
|
|
@ -21,6 +21,7 @@ const loaders = {
|
|||
ga: () => import('./ga.json'),
|
||||
he: () => import('./he.json'),
|
||||
hu: () => import('./hu.json'),
|
||||
id: () => import('./id.json'),
|
||||
it: () => import('./it.json'),
|
||||
ja: () => import('./ja_pedantic.json'),
|
||||
ja_easy: () => import('./ja_easy.json'),
|
||||
|
|
|
@ -918,7 +918,6 @@
|
|||
"upload_a_photo": "Foto uploaden",
|
||||
"useStreamingApi": "Berichten en meldingen in real-time ontvangen",
|
||||
"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_contain_fit": "Bijlage in miniaturen niet bijsnijden",
|
||||
"use_one_click_nsfw": "Gevoelige bijlagen met slechts één klik openen",
|
||||
|
|
|
@ -922,7 +922,6 @@
|
|||
"upload_a_photo": "Вивантажити фото",
|
||||
"useStreamingApi": "Отримувати дописи та сповіщення наживо",
|
||||
"useStreamingApiWarning": "Загалом працює. Якщо не зовсім, спробуєте оновити сторінку?",
|
||||
"use_at_icon": "Значок {'@'} замість символу",
|
||||
"use_blurhash": "Показувати дражливі мініатюри (як розмиті кольори)",
|
||||
"use_contain_fit": "Не обрізати краї мініатюр",
|
||||
"use_one_click_nsfw": "Відкривати NSFW вкладення одним кліком миші",
|
||||
|
|
|
@ -922,7 +922,6 @@
|
|||
"upload_a_photo": "上传照片",
|
||||
"useStreamingApi": "实时接收帖文和通知",
|
||||
"useStreamingApiWarning": "十分炫酷推荐使用。要是崩了试试刷新?",
|
||||
"use_at_icon": "将 {'@'} 符号显示为图标而不是文本",
|
||||
"use_blurhash": "对NSFW的缩略图使用模糊处理",
|
||||
"use_contain_fit": "生成缩略图时不要裁剪附件",
|
||||
"use_one_click_nsfw": "点击一次以打开工作场所不适宜(NSFW)的附件",
|
||||
|
|
16
src/lib/post_language.js
Normal file
16
src/lib/post_language.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import iso6391 from 'iso-639-1'
|
||||
import { computed } from 'vue'
|
||||
|
||||
export const usePostLanguageOptions = () => {
|
||||
const postLanguageOptions = computed(() => {
|
||||
return iso6391.getAllCodes().map(lang => ({
|
||||
key: lang,
|
||||
value: lang,
|
||||
label: lang,
|
||||
}));
|
||||
})
|
||||
|
||||
return {
|
||||
postLanguageOptions,
|
||||
}
|
||||
}
|
|
@ -55,6 +55,7 @@ export const defaultState = {
|
|||
alwaysShowNewPostButton: false,
|
||||
autohideFloatingPostButton: false,
|
||||
pauseOnUnfocused: true,
|
||||
displayPageBackgrounds: true,
|
||||
stopGifs: undefined,
|
||||
replyVisibility: 'all',
|
||||
thirdColumnMode: 'notifications',
|
||||
|
@ -95,7 +96,6 @@ export const defaultState = {
|
|||
disableStickyHeaders: false,
|
||||
showScrollbars: false,
|
||||
greentext: undefined, // instance default
|
||||
useAtIcon: undefined, // instance default
|
||||
mentionLinkDisplay: undefined, // instance default
|
||||
mentionLinkShowTooltip: undefined, // instance default
|
||||
mentionLinkShowAvatar: undefined, // instance default
|
||||
|
@ -116,6 +116,7 @@ export const defaultState = {
|
|||
conversationTreeFadeAncestors: undefined, // instance default
|
||||
maxDepthInThread: undefined, // instance default
|
||||
translationLanguage: undefined, // instance default,
|
||||
postLanguage: undefined, // instance default,
|
||||
supportedTranslationLanguages: {}, // instance default
|
||||
userProfileDefaultTab: 'statuses',
|
||||
useBlurhash: true,
|
||||
|
@ -229,7 +230,7 @@ const config = {
|
|||
break
|
||||
case 'interfaceLanguage':
|
||||
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 })
|
||||
break
|
||||
case 'thirdColumnMode':
|
||||
|
|
|
@ -21,7 +21,6 @@ const defaultState = {
|
|||
background: '/static/aurora_borealis.jpg',
|
||||
collapseMessageWithSubject: true,
|
||||
greentext: false,
|
||||
useAtIcon: false,
|
||||
mentionLinkDisplay: 'short',
|
||||
mentionLinkShowTooltip: true,
|
||||
mentionLinkShowAvatar: false,
|
||||
|
|
|
@ -47,6 +47,10 @@ export const settingsMap = {
|
|||
},
|
||||
// Privacy
|
||||
'locked': 'locked',
|
||||
'permitFollowback': {
|
||||
get: 'akkoma.permit_followback',
|
||||
set: 'permit_followback'
|
||||
},
|
||||
'allowFollowingMove': {
|
||||
get: 'pleroma.allow_following_move',
|
||||
set: 'allow_following_move'
|
||||
|
|
|
@ -314,7 +314,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
})
|
||||
|
||||
// Keep the visible statuses sorted
|
||||
if (timeline && !(timeline === 'bookmarks')) {
|
||||
if (timeline && !(['bookmarks', 'favorites'].includes(timeline))) {
|
||||
sortTimeline(timelineObject)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -135,6 +135,10 @@ export const mutations = {
|
|||
const user = state.usersObject[id]
|
||||
user['deactivated'] = deactivated
|
||||
},
|
||||
setDisplayBackground(state, url) {
|
||||
console.log("Commiting user profile bg mutation")
|
||||
state.displayBackground = url
|
||||
},
|
||||
setCurrentUser (state, user) {
|
||||
state.lastLoginName = user.screen_name
|
||||
state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength)
|
||||
|
@ -307,6 +311,7 @@ export const defaultState = {
|
|||
currentUser: false,
|
||||
users: [],
|
||||
usersObject: {},
|
||||
displayBackground: null,
|
||||
signUpPending: false,
|
||||
signUpErrors: [],
|
||||
relationships: {},
|
||||
|
@ -319,6 +324,10 @@ const users = {
|
|||
mutations,
|
||||
getters,
|
||||
actions: {
|
||||
setDisplayBackground (store, url) {
|
||||
console.log("Performing user profile bg action...")
|
||||
store.commit('setDisplayBackground', url)
|
||||
},
|
||||
fetchUserIfMissing (store, id) {
|
||||
if (!store.getters.findUser(id)) {
|
||||
store.dispatch('fetchUser', id)
|
||||
|
|
|
@ -95,6 +95,7 @@ export const parseUser = (data) => {
|
|||
if (data.akkoma) {
|
||||
output.instance = data.akkoma.instance
|
||||
output.status_ttl_days = data.akkoma.status_ttl_days
|
||||
output.permit_followback = data.akkoma.permit_followback
|
||||
}
|
||||
|
||||
if (data.pleroma) {
|
||||
|
|
|
@ -8,7 +8,30 @@ const specialLanguageCodes = {
|
|||
'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('_', '-')
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue