Compare commits

...

6 commits

15 changed files with 101 additions and 5 deletions

View file

@ -275,6 +275,7 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') })
const uploadLimits = metadata.uploadLimits const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })

View file

@ -8,7 +8,6 @@
<button <button
class="emoji-reaction btn button-default" class="emoji-reaction btn button-default"
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }" :class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
:disabled="!isLocalReaction(reaction.url)"
@click="emojiOnClick(reaction.name, $event)" @click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()" @mouseenter="fetchEmojiReactionsByIfMissing()"
> >

View file

@ -55,6 +55,13 @@ const ExtraButtons = {
hideDeleteStatusConfirmDialog () { hideDeleteStatusConfirmDialog () {
this.showingDeleteDialog = false this.showingDeleteDialog = false
}, },
translateStatus () {
const translateTo = this.$store.getters.mergedConfig.translationLanguage || this.$store.state.instance.interfaceLanguage
this.$store.dispatch('translateStatus', { id: this.status.id, language: translateTo })
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
},
pinStatus () { pinStatus () {
this.$store.dispatch('pinStatus', this.status.id) this.$store.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
@ -110,6 +117,9 @@ const ExtraButtons = {
canMute () { canMute () {
return !!this.currentUser return !!this.currentUser
}, },
canTranslate () {
return this.$store.state.instance.translationEnabled === true
},
statusLink () { statusLink () {
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}` return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
}, },

View file

@ -116,6 +116,17 @@
:icon="['far', 'flag']" :icon="['far', 'flag']"
/><span>{{ $t("user_card.report") }}</span> /><span>{{ $t("user_card.report") }}</span>
</button> </button>
<button
v-if="canTranslate"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="translateStatus"
@click="close"
>
<FAIcon
fixed-width
icon="globe"
/><span>{{ $t("status.translate") }}</span>
</button>
</div> </div>
</template> </template>
<template v-slot:trigger> <template v-slot:trigger>

View file

@ -1,6 +1,10 @@
<template> <template>
<div> <div>
<FAIcon icon="globe" /> {{ ' ' }} <FAIcon
v-if="globeIcon"
icon="globe"
/>
{{ ' ' }}
<label for="interface-language-switcher"> <label for="interface-language-switcher">
{{ promptText }} {{ promptText }}
</label> </label>
@ -40,6 +44,10 @@ export default {
setLanguage: { setLanguage: {
type: Function, type: Function,
required: true required: true
},
globeIcon: {
type: Boolean,
default: true
} }
}, },
computed: { computed: {

View file

@ -1,6 +1,6 @@
<template> <template>
<div <div
v-if="federationPolicy" v-if="hasInstanceSpecificPolicies"
class="mrf-transparency-panel" class="mrf-transparency-panel"
> >
<div class="panel panel-default base01-background"> <div class="panel panel-default base01-background">

View file

@ -50,6 +50,7 @@ const GeneralTab = {
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') || Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
// Future spec, still not supported in Nightly 63 as of 08/2018 // Future spec, still not supported in Nightly 63 as of 08/2018
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks') Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
} }
}, },
components: { components: {
@ -82,11 +83,20 @@ const GeneralTab = {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
} }
}, },
translationLanguage: {
get: function () { return this.$store.getters.mergedConfig.translationLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'translationLanguage', value: val })
}
},
...SharedComputedObject() ...SharedComputedObject()
}, },
methods: { methods: {
changeDefaultScope (value) { changeDefaultScope (value) {
this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value }) this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value })
},
setTranslationLanguage (value) {
this.$store.dispatch('setOption', { name: 'translationLanguage', value })
} }
} }
} }

View file

@ -156,6 +156,16 @@
</li> </li>
</ul> </ul>
</li> </li>
<li>
<p>
<interface-language-switcher
:globe-icon="false"
:prompt-text="$t('settings.translation_language')"
:language="translationLanguage"
:set-language="setTranslationLanguage"
/>
</p>
</li>
<li> <li>
<BooleanSetting <BooleanSetting
path="alwaysShowNewPostButton" path="alwaysShowNewPostButton"

View file

@ -4,6 +4,12 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.translation {
border: 1px solid var(--accent, $fallback--link);
border-radius: var(--panelRadius, $fallback--panelRadius);
margin-top: 1em;
padding: 0.5em;
}
.emoji { .emoji {
--_still_image-label-scale: 0.5; --_still_image-label-scale: 0.5;
--emoji-size: 38px; --emoji-size: 38px;

View file

@ -56,6 +56,23 @@
:attentions="status.attentions" :attentions="status.attentions"
@parseReady="onParseReady" @parseReady="onParseReady"
/> />
<div
v-if="status.translation"
class="translation"
>
<h4>{{ $t('status.translated_from', { language: status.translation.detected_language }) }}</h4>
<RichContent
:class="{ '-single-line': singleLine }"
class="text media-body"
:html="status.translation.text"
:emoji="status.emojis"
:handle-links="true"
:mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')"
:greentext="mergedConfig.greentext"
:attentions="status.attentions"
@parseReady="onParseReady"
/>
</div>
</div> </div>
<button <button
v-show="hideSubjectStatus" v-show="hideSubjectStatus"

View file

@ -794,6 +794,7 @@
"third_column_mode_postform": "Main post form and navigation", "third_column_mode_postform": "Main post form and navigation",
"token": "Token", "token": "Token",
"tooltipRadius": "Tooltips/alerts", "tooltipRadius": "Tooltips/alerts",
"translation_language": "Automatic Translation Language",
"tree_advanced": "Allow more flexible navigation in tree view", "tree_advanced": "Allow more flexible navigation in tree view",
"tree_fade_ancestors": "Display ancestors of the current post in faint text", "tree_fade_ancestors": "Display ancestors of the current post in faint text",
"type_domains_to_mute": "Search domains to mute", "type_domains_to_mute": "Search domains to mute",
@ -875,6 +876,8 @@
"thread_show": "Show this thread", "thread_show": "Show this thread",
"thread_show_full": "Show everything under this thread ({numStatus} post in total, max depth {depth}) | Show everything under this thread ({numStatus} posts in total, max depth {depth})", "thread_show_full": "Show everything under this thread ({numStatus} post in total, max depth {depth}) | Show everything under this thread ({numStatus} posts in total, max depth {depth})",
"thread_show_full_with_icon": "{icon} {text}", "thread_show_full_with_icon": "{icon} {text}",
"translate": "Translate",
"translated_from": "Translated from {language}",
"unbookmark": "Unbookmark", "unbookmark": "Unbookmark",
"unmute_conversation": "Unmute conversation", "unmute_conversation": "Unmute conversation",
"unpin": "Unpin from profile", "unpin": "Unpin from profile",

View file

@ -791,6 +791,8 @@
"status_unavailable": "利用できません", "status_unavailable": "利用できません",
"thread_muted": "ミュートされたスレッド", "thread_muted": "ミュートされたスレッド",
"thread_muted_and_words": "以下の単語を含むため:", "thread_muted_and_words": "以下の単語を含むため:",
"translate": "翻訳",
"translated_from": "{language}から翻訳されました",
"unbookmark": "ブックマーク解除", "unbookmark": "ブックマーク解除",
"unmute_conversation": "スレッドのミュートを解除", "unmute_conversation": "スレッドのミュートを解除",
"unpin": "プロフィールのピン留めを外す", "unpin": "プロフィールのピン留めを外す",

View file

@ -114,7 +114,8 @@ export const defaultState = {
conversationTreeAdvanced: undefined, // instance default conversationTreeAdvanced: undefined, // instance default
conversationOtherRepliesButton: undefined, // instance default conversationOtherRepliesButton: undefined, // instance default
conversationTreeFadeAncestors: undefined, // instance default conversationTreeFadeAncestors: undefined, // instance default
maxDepthInThread: undefined // instance default maxDepthInThread: undefined, // instance default
translationLanguage: undefined // instance default
} }
// caching the instance default properties // caching the instance default properties
@ -187,6 +188,7 @@ const config = {
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))
dispatch('setInstanceOption', { name: 'interfaceLanguage', value })
break break
case 'thirdColumnMode': case 'thirdColumnMode':
dispatch('setLayoutWidth', undefined) dispatch('setLayoutWidth', undefined)

View file

@ -425,6 +425,10 @@ export const mutations = {
state.conversationsObject[newStatus.statusnet_conversation_id].forEach(status => { status.thread_muted = newStatus.thread_muted }) state.conversationsObject[newStatus.statusnet_conversation_id].forEach(status => { status.thread_muted = newStatus.thread_muted })
} }
}, },
setTranslatedStatus (state, { id, translation }) {
const newStatus = state.allStatusesObject[id]
newStatus.translation = translation
},
setRetweeted (state, { status, value }) { setRetweeted (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id] const newStatus = state.allStatusesObject[status.id]
@ -637,6 +641,10 @@ const statuses = {
rootState.api.backendInteractor.unpinOwnStatus({ id: statusId }) rootState.api.backendInteractor.unpinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] })) .then((status) => dispatch('addNewStatuses', { statuses: [status] }))
}, },
translateStatus ({ rootState, commit }, { id, translation, language }) {
return rootState.api.backendInteractor.translateStatus({ id: id, translation, language })
.then((translation) => commit('setTranslatedStatus', { id, translation }))
},
muteConversation ({ rootState, commit }, statusId) { muteConversation ({ rootState, commit }, statusId) {
return rootState.api.backendInteractor.muteConversation({ id: statusId }) return rootState.api.backendInteractor.muteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status)) .then((status) => commit('setMutedStatus', status))

View file

@ -31,6 +31,7 @@ const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
const MASTODON_REGISTRATION_URL = '/api/v1/accounts' const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications' const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
const AKKOMA_TRANSLATE_URL = (id, lang) => `/api/v1/statuses/${id}/translations/${lang}`
const MASTODON_DISMISS_NOTIFICATION_URL = id => `/api/v1/notifications/${id}/dismiss` const MASTODON_DISMISS_NOTIFICATION_URL = id => `/api/v1/notifications/${id}/dismiss`
const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite` const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite` const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
@ -738,6 +739,13 @@ const unretweet = ({ id, credentials }) => {
.then((data) => parseStatus(data)) .then((data) => parseStatus(data))
} }
const translateStatus = ({ id, credentials, language }) => {
return promisedRequest({ url: AKKOMA_TRANSLATE_URL(id, language), method: 'GET', credentials })
.then((data) => {
return data
})
}
const bookmarkStatus = ({ id, credentials }) => { const bookmarkStatus = ({ id, credentials }) => {
return promisedRequest({ return promisedRequest({
url: MASTODON_BOOKMARK_STATUS_URL(id), url: MASTODON_BOOKMARK_STATUS_URL(id),
@ -1576,7 +1584,8 @@ const apiService = {
postAnnouncement, postAnnouncement,
editAnnouncement, editAnnouncement,
deleteAnnouncement, deleteAnnouncement,
adminFetchAnnouncements adminFetchAnnouncements,
translateStatus
} }
export default apiService export default apiService