Merge remote-tracking branch 'upstream/develop' into masto-api/direct
* upstream/develop: (47 commits) #449 - fix auth token fetch issue Make select tag use --inputText as text color #444 - remote follow clean up #444 - show `remote follow` button when logged out Add button to save without cropping post-merge fixes [i18n] Update oc.json after store: fix setting postFormats field fix user-card avatar falling into permament failed state fix flake id users not fetching correctly fix console error afterStoreSetup: Move log in and theme load to afterStoreSetup. afterStoreSetup: Handle 404 cases. afterStoreSetup: Emoji and nodeinfo refactor. afterStoreSetup: refactor TOS and panel fetching, handle 404s. afterStoreSetup: refactor. Load persistedStated with async/await. whoops レインせんぱいにサンキュー fix embedded relationship card parsing ...
This commit is contained in:
commit
faef769b40
76 changed files with 1289 additions and 599 deletions
|
@ -8,6 +8,7 @@ import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_pan
|
|||
import ChatPanel from './components/chat_panel/chat_panel.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import MobilePostStatusModal from './components/mobile_post_status_modal/mobile_post_status_modal.vue'
|
||||
import { unseenNotificationsFromStore } from './services/notification_utils/notification_utils'
|
||||
|
||||
export default {
|
||||
|
@ -22,7 +23,8 @@ export default {
|
|||
WhoToFollowPanel,
|
||||
ChatPanel,
|
||||
MediaModal,
|
||||
SideDrawer
|
||||
SideDrawer,
|
||||
MobilePostStatusModal
|
||||
},
|
||||
data: () => ({
|
||||
mobileActivePanel: 'timeline',
|
||||
|
|
27
src/App.scss
27
src/App.scss
|
@ -154,7 +154,7 @@ input, textarea, .select {
|
|||
background: transparent;
|
||||
border: none;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
color: var(--inputText, --text, $fallback--text);
|
||||
margin: 0;
|
||||
padding: 0 2em 0 .2em;
|
||||
font-family: sans-serif;
|
||||
|
@ -671,6 +671,31 @@ nav {
|
|||
border-radius: var(--inputRadius, $fallback--inputRadius);
|
||||
}
|
||||
|
||||
@keyframes modal-background-fadein {
|
||||
from {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
to {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-view {
|
||||
z-index: 1000;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: auto;
|
||||
animation-duration: 0.2s;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
animation-name: modal-background-fadein;
|
||||
}
|
||||
|
||||
.button-icon {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
<media-modal></media-modal>
|
||||
</div>
|
||||
<chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel>
|
||||
<MobilePostStatusModal />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -4,10 +4,11 @@ import routes from './routes'
|
|||
|
||||
import App from '../App.vue'
|
||||
|
||||
const afterStoreSetup = ({ store, i18n }) => {
|
||||
window.fetch('/api/statusnet/config.json')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const getStatusnetConfig = async ({ store }) => {
|
||||
try {
|
||||
const res = await window.fetch('/api/statusnet/config.json')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey } = data.site
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'name', value: name })
|
||||
|
@ -28,140 +29,167 @@ const afterStoreSetup = ({ store, i18n }) => {
|
|||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||
}
|
||||
|
||||
var apiConfig = data.site.pleromafe
|
||||
return data.site.pleromafe
|
||||
} else {
|
||||
throw (res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Could not load statusnet config, potentially fatal')
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
window.fetch('/static/config.json')
|
||||
.then((res) => res.json())
|
||||
.catch((err) => {
|
||||
console.warn('Failed to load static/config.json, continuing without it.')
|
||||
console.warn(err)
|
||||
return {}
|
||||
})
|
||||
.then((staticConfig) => {
|
||||
const overrides = window.___pleromafe_dev_overrides || {}
|
||||
const env = window.___pleromafe_mode.NODE_ENV
|
||||
const getStaticConfig = async () => {
|
||||
try {
|
||||
const res = await window.fetch('/static/config.json')
|
||||
if (res.ok) {
|
||||
return res.json()
|
||||
} else {
|
||||
throw (res)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load static/config.json, continuing without it.')
|
||||
console.warn(error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// This takes static config and overrides properties that are present in apiConfig
|
||||
let config = {}
|
||||
if (overrides.staticConfigPreference && env === 'development') {
|
||||
console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG')
|
||||
config = Object.assign({}, apiConfig, staticConfig)
|
||||
} else {
|
||||
config = Object.assign({}, staticConfig, apiConfig)
|
||||
}
|
||||
const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
||||
const overrides = window.___pleromafe_dev_overrides || {}
|
||||
const env = window.___pleromafe_mode.NODE_ENV
|
||||
|
||||
const copyInstanceOption = (name) => {
|
||||
store.dispatch('setInstanceOption', {name, value: config[name]})
|
||||
}
|
||||
// This takes static config and overrides properties that are present in apiConfig
|
||||
let config = {}
|
||||
if (overrides.staticConfigPreference && env === 'development') {
|
||||
console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG')
|
||||
config = Object.assign({}, apiConfig, staticConfig)
|
||||
} else {
|
||||
config = Object.assign({}, staticConfig, apiConfig)
|
||||
}
|
||||
|
||||
copyInstanceOption('nsfwCensorImage')
|
||||
copyInstanceOption('background')
|
||||
copyInstanceOption('hidePostStats')
|
||||
copyInstanceOption('hideUserStats')
|
||||
copyInstanceOption('hideFilteredStatuses')
|
||||
copyInstanceOption('logo')
|
||||
const copyInstanceOption = (name) => {
|
||||
store.dispatch('setInstanceOption', { name, value: config[name] })
|
||||
}
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMask',
|
||||
value: typeof config.logoMask === 'undefined'
|
||||
? true
|
||||
: config.logoMask
|
||||
})
|
||||
copyInstanceOption('nsfwCensorImage')
|
||||
copyInstanceOption('background')
|
||||
copyInstanceOption('hidePostStats')
|
||||
copyInstanceOption('hideUserStats')
|
||||
copyInstanceOption('hideFilteredStatuses')
|
||||
copyInstanceOption('logo')
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMargin',
|
||||
value: typeof config.logoMargin === 'undefined'
|
||||
? 0
|
||||
: config.logoMargin
|
||||
})
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMask',
|
||||
value: typeof config.logoMask === 'undefined'
|
||||
? true
|
||||
: config.logoMask
|
||||
})
|
||||
|
||||
copyInstanceOption('redirectRootNoLogin')
|
||||
copyInstanceOption('redirectRootLogin')
|
||||
copyInstanceOption('showInstanceSpecificPanel')
|
||||
copyInstanceOption('scopeOptionsEnabled')
|
||||
copyInstanceOption('formattingOptionsEnabled')
|
||||
copyInstanceOption('collapseMessageWithSubject')
|
||||
copyInstanceOption('loginMethod')
|
||||
copyInstanceOption('scopeCopy')
|
||||
copyInstanceOption('subjectLineBehavior')
|
||||
copyInstanceOption('postContentType')
|
||||
copyInstanceOption('alwaysShowSubjectInput')
|
||||
copyInstanceOption('noAttachmentLinks')
|
||||
copyInstanceOption('showFeaturesPanel')
|
||||
store.dispatch('setInstanceOption', {
|
||||
name: 'logoMargin',
|
||||
value: typeof config.logoMargin === 'undefined'
|
||||
? 0
|
||||
: config.logoMargin
|
||||
})
|
||||
|
||||
if ((config.chatDisabled)) {
|
||||
store.dispatch('disableChat')
|
||||
} else {
|
||||
store.dispatch('initializeSocket')
|
||||
}
|
||||
copyInstanceOption('redirectRootNoLogin')
|
||||
copyInstanceOption('redirectRootLogin')
|
||||
copyInstanceOption('showInstanceSpecificPanel')
|
||||
copyInstanceOption('scopeOptionsEnabled')
|
||||
copyInstanceOption('formattingOptionsEnabled')
|
||||
copyInstanceOption('collapseMessageWithSubject')
|
||||
copyInstanceOption('loginMethod')
|
||||
copyInstanceOption('scopeCopy')
|
||||
copyInstanceOption('subjectLineBehavior')
|
||||
copyInstanceOption('postContentType')
|
||||
copyInstanceOption('alwaysShowSubjectInput')
|
||||
copyInstanceOption('noAttachmentLinks')
|
||||
copyInstanceOption('showFeaturesPanel')
|
||||
|
||||
return store.dispatch('setTheme', config['theme'])
|
||||
})
|
||||
.then(() => {
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
routes: routes(store),
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
if (to.matched.some(m => m.meta.dontScroll)) {
|
||||
return false
|
||||
}
|
||||
return savedPosition || { x: 0, y: 0 }
|
||||
}
|
||||
})
|
||||
if ((config.chatDisabled)) {
|
||||
store.dispatch('disableChat')
|
||||
} else {
|
||||
store.dispatch('initializeSocket')
|
||||
}
|
||||
|
||||
/* eslint-disable no-new */
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
i18n,
|
||||
el: '#app',
|
||||
render: h => h(App)
|
||||
})
|
||||
})
|
||||
})
|
||||
return store.dispatch('setTheme', config['theme'])
|
||||
}
|
||||
|
||||
window.fetch('/static/terms-of-service.html')
|
||||
.then((res) => res.text())
|
||||
.then((html) => {
|
||||
const getTOS = async ({ store }) => {
|
||||
try {
|
||||
const res = await window.fetch('/static/terms-of-service.html')
|
||||
if (res.ok) {
|
||||
const html = await res.text()
|
||||
store.dispatch('setInstanceOption', { name: 'tos', value: html })
|
||||
})
|
||||
} else {
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load TOS")
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
window.fetch('/api/pleroma/emoji.json')
|
||||
.then(
|
||||
(res) => res.json()
|
||||
.then(
|
||||
(values) => {
|
||||
const emoji = Object.keys(values).map((key) => {
|
||||
return { shortcode: key, image_url: values[key] }
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true })
|
||||
},
|
||||
(failure) => {
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false })
|
||||
}
|
||||
),
|
||||
(error) => console.log(error)
|
||||
)
|
||||
const getInstancePanel = async ({ store }) => {
|
||||
try {
|
||||
const res = await window.fetch('/instance/panel.html')
|
||||
if (res.ok) {
|
||||
const html = await res.text()
|
||||
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
|
||||
} else {
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load instance panel")
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
window.fetch('/static/emoji.json')
|
||||
.then((res) => res.json())
|
||||
.then((values) => {
|
||||
const getStaticEmoji = async ({ store }) => {
|
||||
try {
|
||||
const res = await window.fetch('/static/emoji.json')
|
||||
if (res.ok) {
|
||||
const values = await res.json()
|
||||
const emoji = Object.keys(values).map((key) => {
|
||||
return { shortcode: key, image_url: false, 'utf': values[key] }
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'emoji', value: emoji })
|
||||
})
|
||||
} else {
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load static emoji")
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
window.fetch('/instance/panel.html')
|
||||
.then((res) => res.text())
|
||||
.then((html) => {
|
||||
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
|
||||
})
|
||||
// This is also used to indicate if we have a 'pleroma backend' or not.
|
||||
// Somewhat weird, should probably be somewhere else.
|
||||
const getCustomEmoji = async ({ store }) => {
|
||||
try {
|
||||
const res = await window.fetch('/api/pleroma/emoji.json')
|
||||
if (res.ok) {
|
||||
const values = await res.json()
|
||||
const emoji = Object.keys(values).map((key) => {
|
||||
return { shortcode: key, image_url: values[key] }
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true })
|
||||
} else {
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false })
|
||||
console.warn("Can't load custom emojis, maybe not a Pleroma instance?")
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
window.fetch('/nodeinfo/2.0.json')
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
const getNodeInfo = async ({ store }) => {
|
||||
try {
|
||||
const res = await window.fetch('/nodeinfo/2.0.json')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
const metadata = data.metadata
|
||||
|
||||
const features = metadata.features
|
||||
|
@ -169,14 +197,71 @@ const afterStoreSetup = ({ store, i18n }) => {
|
|||
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
|
||||
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
|
||||
|
||||
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
|
||||
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })
|
||||
|
||||
const suggestions = metadata.suggestions
|
||||
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
|
||||
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
|
||||
|
||||
const software = data.software
|
||||
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
|
||||
|
||||
const frontendVersion = window.___pleromafe_commit_hash
|
||||
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
|
||||
} else {
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not load nodeinfo')
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
const afterStoreSetup = async ({ store, i18n }) => {
|
||||
if (store.state.config.customTheme) {
|
||||
// This is a hack to deal with async loading of config.json and themes
|
||||
// See: style_setter.js, setPreset()
|
||||
window.themeLoaded = true
|
||||
store.dispatch('setOption', {
|
||||
name: 'customTheme',
|
||||
value: store.state.config.customTheme
|
||||
})
|
||||
}
|
||||
|
||||
const apiConfig = await getStatusnetConfig({ store })
|
||||
const staticConfig = await getStaticConfig()
|
||||
await setSettings({ store, apiConfig, staticConfig })
|
||||
await getTOS({ store })
|
||||
await getInstancePanel({ store })
|
||||
await getStaticEmoji({ store })
|
||||
await getCustomEmoji({ store })
|
||||
await getNodeInfo({ store })
|
||||
|
||||
// Now we have the server settings and can try logging in
|
||||
if (store.state.oauth.token) {
|
||||
store.dispatch('loginUser', store.state.oauth.token)
|
||||
}
|
||||
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
routes: routes(store),
|
||||
scrollBehavior: (to, _from, savedPosition) => {
|
||||
if (to.matched.some(m => m.meta.dontScroll)) {
|
||||
return false
|
||||
}
|
||||
return savedPosition || { x: 0, y: 0 }
|
||||
}
|
||||
})
|
||||
|
||||
/* eslint-disable no-new */
|
||||
return new Vue({
|
||||
router,
|
||||
store,
|
||||
i18n,
|
||||
el: '#app',
|
||||
render: h => h(App)
|
||||
})
|
||||
}
|
||||
|
||||
export default afterStoreSetup
|
||||
|
|
|
@ -13,7 +13,6 @@ import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
|||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||
import UserSearch from 'components/user_search/user_search.vue'
|
||||
import Notifications from 'components/notifications/notifications.vue'
|
||||
import UserPanel from 'components/user_panel/user_panel.vue'
|
||||
import LoginForm from 'components/login_form/login_form.vue'
|
||||
import ChatPanel from 'components/chat_panel/chat_panel.vue'
|
||||
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
||||
|
@ -43,7 +42,6 @@ export default (store) => {
|
|||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
|
||||
{ name: 'user-settings', path: '/user-settings', component: UserSettings },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications },
|
||||
{ name: 'new-status', path: '/:username/new-status', component: UserPanel },
|
||||
{ name: 'login', path: '/login', component: LoginForm },
|
||||
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||
|
|
|
@ -9,7 +9,7 @@ const BlockCard = {
|
|||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.getters.userById(this.userId)
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
blocked () {
|
||||
return this.user.statusnet_blocking
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
||||
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||
|
||||
const FollowCard = {
|
||||
|
@ -14,13 +15,17 @@ const FollowCard = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
BasicUserCard
|
||||
BasicUserCard,
|
||||
RemoteFollow
|
||||
},
|
||||
computed: {
|
||||
isMe () { return this.$store.state.users.currentUser.id === this.user.id },
|
||||
following () { return this.updated ? this.updated.following : this.user.following },
|
||||
showFollow () {
|
||||
return !this.following || this.updated && !this.updated.following
|
||||
},
|
||||
loggedIn () {
|
||||
return this.$store.state.users.currentUser
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -4,9 +4,12 @@
|
|||
<span class="faint" v-if="!noFollowsYou && user.follows_you">
|
||||
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||
</span>
|
||||
<div class="btn-follow" v-if="showFollow && !loggedIn">
|
||||
<RemoteFollow :user="user" />
|
||||
</div>
|
||||
<button
|
||||
v-if="showFollow"
|
||||
class="btn btn-default"
|
||||
v-if="showFollow && loggedIn"
|
||||
class="btn btn-default btn-follow"
|
||||
@click="followUser"
|
||||
:disabled="inProgress"
|
||||
:title="requestSent ? $t('user_card.follow_again') : ''"
|
||||
|
@ -44,7 +47,7 @@
|
|||
flex-wrap: wrap;
|
||||
line-height: 1.5em;
|
||||
|
||||
.btn {
|
||||
.btn-follow {
|
||||
margin-top: 0.5em;
|
||||
margin-left: auto;
|
||||
width: 10em;
|
||||
|
|
|
@ -31,6 +31,9 @@ const ImageCropper = {
|
|||
saveButtonLabel: {
|
||||
type: String
|
||||
},
|
||||
saveWithoutCroppingButtonlabel: {
|
||||
type: String
|
||||
},
|
||||
cancelButtonLabel: {
|
||||
type: String
|
||||
}
|
||||
|
@ -48,6 +51,9 @@ const ImageCropper = {
|
|||
saveText () {
|
||||
return this.saveButtonLabel || this.$t('image_cropper.save')
|
||||
},
|
||||
saveWithoutCroppingText () {
|
||||
return this.saveWithoutCroppingButtonlabel || this.$t('image_cropper.save_without_cropping')
|
||||
},
|
||||
cancelText () {
|
||||
return this.cancelButtonLabel || this.$t('image_cropper.cancel')
|
||||
},
|
||||
|
@ -76,6 +82,18 @@ const ImageCropper = {
|
|||
this.submitting = false
|
||||
})
|
||||
},
|
||||
submitWithoutCropping () {
|
||||
this.submitting = true
|
||||
this.avatarUploadError = null
|
||||
this.submitHandler(false, this.dataUrl)
|
||||
.then(() => this.destroy())
|
||||
.catch((err) => {
|
||||
this.submitError = err
|
||||
})
|
||||
.finally(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
},
|
||||
pickImage () {
|
||||
this.$refs.input.click()
|
||||
},
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<div class="image-cropper-buttons-wrapper">
|
||||
<button class="btn" type="button" :disabled="submitting" @click="submit" v-text="saveText"></button>
|
||||
<button class="btn" type="button" :disabled="submitting" @click="destroy" v-text="cancelText"></button>
|
||||
<button class="btn" type="button" :disabled="submitting" @click="submitWithoutCropping" v-text="saveWithoutCroppingText"></button>
|
||||
<i class="icon-spin4 animate-spin" v-if="submitting"></i>
|
||||
</div>
|
||||
<div class="alert error" v-if="submitError">
|
||||
|
@ -36,7 +37,11 @@
|
|||
}
|
||||
|
||||
&-buttons-wrapper {
|
||||
margin-top: 15px;
|
||||
margin-top: 10px;
|
||||
|
||||
button {
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="modal-view" v-if="showing" @click.prevent="hide">
|
||||
<div class="modal-view media-modal-view" v-if="showing" @click.prevent="hide">
|
||||
<img class="modal-image" v-if="type === 'image'" :src="currentMedia.url"></img>
|
||||
<VideoAttachment
|
||||
class="modal-image"
|
||||
|
@ -32,18 +32,7 @@
|
|||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.modal-view {
|
||||
z-index: 1000;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
|
||||
.media-modal-view {
|
||||
&:hover {
|
||||
.modal-view-button-arrow {
|
||||
opacity: 0.75;
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import { throttle } from 'lodash'
|
||||
|
||||
const MobilePostStatusModal = {
|
||||
components: {
|
||||
PostStatusForm
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
hidden: false,
|
||||
postFormOpen: false,
|
||||
scrollingDown: false,
|
||||
inputActive: false,
|
||||
oldScrollPos: 0,
|
||||
amountScrolled: 0
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('scroll', this.handleScroll)
|
||||
window.addEventListener('resize', this.handleOSK)
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('scroll', this.handleScroll)
|
||||
window.removeEventListener('resize', this.handleOSK)
|
||||
},
|
||||
computed: {
|
||||
currentUser () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
isHidden () {
|
||||
return this.hidden || this.inputActive
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openPostForm () {
|
||||
this.postFormOpen = true
|
||||
this.hidden = true
|
||||
|
||||
const el = this.$el.querySelector('textarea')
|
||||
this.$nextTick(function () {
|
||||
el.focus()
|
||||
})
|
||||
},
|
||||
closePostForm () {
|
||||
this.postFormOpen = false
|
||||
this.hidden = false
|
||||
},
|
||||
handleOSK () {
|
||||
// This is a big hack: we're guessing from changed window sizes if the
|
||||
// on-screen keyboard is active or not. This is only really important
|
||||
// for phones in portrait mode and it's more important to show the button
|
||||
// in normal scenarios on all phones, than it is to hide it when the
|
||||
// keyboard is active.
|
||||
// Guesswork based on https://www.mydevice.io/#compare-devices
|
||||
|
||||
// for example, iphone 4 and android phones from the same time period
|
||||
const smallPhone = window.innerWidth < 350
|
||||
const smallPhoneKbOpen = smallPhone && window.innerHeight < 345
|
||||
|
||||
const biggerPhone = !smallPhone && window.innerWidth < 450
|
||||
const biggerPhoneKbOpen = biggerPhone && window.innerHeight < 560
|
||||
if (smallPhoneKbOpen || biggerPhoneKbOpen) {
|
||||
this.inputActive = true
|
||||
} else {
|
||||
this.inputActive = false
|
||||
}
|
||||
},
|
||||
handleScroll: throttle(function () {
|
||||
const scrollAmount = window.scrollY - this.oldScrollPos
|
||||
const scrollingDown = scrollAmount > 0
|
||||
|
||||
if (scrollingDown !== this.scrollingDown) {
|
||||
this.amountScrolled = 0
|
||||
this.scrollingDown = scrollingDown
|
||||
if (!scrollingDown) {
|
||||
this.hidden = false
|
||||
}
|
||||
} else if (scrollingDown) {
|
||||
this.amountScrolled += scrollAmount
|
||||
if (this.amountScrolled > 100 && !this.hidden) {
|
||||
this.hidden = true
|
||||
}
|
||||
}
|
||||
|
||||
this.oldScrollPos = window.scrollY
|
||||
this.scrollingDown = scrollingDown
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
|
||||
export default MobilePostStatusModal
|
|
@ -0,0 +1,76 @@
|
|||
<template>
|
||||
<div v-if="currentUser">
|
||||
<div
|
||||
class="post-form-modal-view modal-view"
|
||||
v-show="postFormOpen"
|
||||
@click="closePostForm"
|
||||
>
|
||||
<div class="post-form-modal-panel panel" @click.stop="">
|
||||
<div class="panel-heading">{{$t('post_status.new_status')}}</div>
|
||||
<PostStatusForm class="panel-body" @posted="closePostForm"/>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="new-status-button"
|
||||
:class="{ 'hidden': isHidden }"
|
||||
@click="openPostForm"
|
||||
>
|
||||
<i class="icon-edit" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./mobile_post_status_modal.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.post-form-modal-view {
|
||||
max-height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.post-form-modal-panel {
|
||||
flex-shrink: 0;
|
||||
margin: 25% 0 4em 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.new-status-button {
|
||||
width: 5em;
|
||||
height: 5em;
|
||||
border-radius: 100%;
|
||||
position: fixed;
|
||||
bottom: 1.5em;
|
||||
right: 1.5em;
|
||||
// TODO: this needs its own color, it has to stand out enough and link color
|
||||
// is not very optimal for this particular use.
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btn, $fallback--fg);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.3);
|
||||
z-index: 10;
|
||||
|
||||
transition: 0.35s transform;
|
||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||
|
||||
&.hidden {
|
||||
transform: translateY(150%);
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 1.5em;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (min-width: 801px) {
|
||||
.new-status-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -9,7 +9,7 @@ const MuteCard = {
|
|||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.getters.userById(this.userId)
|
||||
return this.$store.getters.findUser(this.userId)
|
||||
},
|
||||
muted () {
|
||||
return this.user.muted
|
||||
|
|
|
@ -222,6 +222,9 @@ const PostStatusForm = {
|
|||
this.highlighted = 0
|
||||
}
|
||||
},
|
||||
onKeydown (e) {
|
||||
e.stopPropagation()
|
||||
},
|
||||
setCaret ({target: {selectionStart}}) {
|
||||
this.caret = selectionStart
|
||||
},
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
ref="textarea"
|
||||
@click="setCaret"
|
||||
@keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control"
|
||||
@keydown="onKeydown"
|
||||
@keydown.down="cycleForward"
|
||||
@keydown.up="cycleBackward"
|
||||
@keydown.shift.tab="cycleBackward"
|
||||
|
|
10
src/components/remote_follow/remote_follow.js
Normal file
10
src/components/remote_follow/remote_follow.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
export default {
|
||||
props: [ 'user' ],
|
||||
computed: {
|
||||
subscribeUrl () {
|
||||
// eslint-disable-next-line no-undef
|
||||
const serverUrl = new URL(this.user.statusnet_profile_url)
|
||||
return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus`
|
||||
}
|
||||
}
|
||||
}
|
24
src/components/remote_follow/remote_follow.vue
Normal file
24
src/components/remote_follow/remote_follow.vue
Normal file
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<div class="remote-follow">
|
||||
<form method="POST" :action='subscribeUrl'>
|
||||
<input type="hidden" name="nickname" :value="user.screen_name">
|
||||
<input type="hidden" name="profile" value="">
|
||||
<button click="submit" class="remote-button">
|
||||
{{ $t('user_card.remote_follow') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./remote_follow.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.remote-follow {
|
||||
max-width: 220px;
|
||||
|
||||
.remote-button {
|
||||
width: 100%;
|
||||
min-height: 28px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,8 +1,13 @@
|
|||
/* eslint-env browser */
|
||||
import { filter, trim } from 'lodash'
|
||||
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
|
||||
import { filter, trim } from 'lodash'
|
||||
import { extractCommit } from '../../services/version/version.service'
|
||||
|
||||
const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
|
||||
const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
|
||||
|
||||
const settings = {
|
||||
data () {
|
||||
|
@ -78,7 +83,10 @@ const settings = {
|
|||
// Future spec, still not supported in Nightly 63 as of 08/2018
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
|
||||
playVideosInModal: user.playVideosInModal,
|
||||
useContainFit: user.useContainFit
|
||||
useContainFit: user.useContainFit,
|
||||
|
||||
backendVersion: instance.backendVersion,
|
||||
frontendVersion: instance.frontendVersion
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -96,7 +104,13 @@ const settings = {
|
|||
postFormats () {
|
||||
return this.$store.state.instance.postFormats || []
|
||||
},
|
||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }
|
||||
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel },
|
||||
frontendVersionLink () {
|
||||
return pleromaFeCommitUrl + this.frontendVersion
|
||||
},
|
||||
backendVersionLink () {
|
||||
return pleromaBeCommitUrl + extractCommit(this.backendVersion)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
hideAttachmentsLocal (value) {
|
||||
|
|
|
@ -261,6 +261,28 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :label="$t('settings.version.title')" >
|
||||
<div class="setting-item">
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<p>{{$t('settings.version.backend_version')}}</p>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<a :href="backendVersionLink" target="_blank">{{backendVersion}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p>{{$t('settings.version.frontend_version')}}</p>
|
||||
<ul class="option-list">
|
||||
<li>
|
||||
<a :href="frontendVersionLink" target="_blank">{{frontendVersion}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</keep-alive>
|
||||
</div>
|
||||
|
|
|
@ -15,12 +15,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
<li v-if="currentUser" @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'new-status', params: { username: currentUser.screen_name } }">
|
||||
{{ $t("post_status.new_status") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-else @click="toggleDrawer">
|
||||
<li v-if="!currentUser" @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'login' }">
|
||||
{{ $t("login.login") }}
|
||||
</router-link>
|
||||
|
@ -119,14 +114,14 @@
|
|||
}
|
||||
|
||||
.side-drawer-container-open {
|
||||
transition-delay: 0.0s;
|
||||
transition-property: left;
|
||||
transition: 0.35s;
|
||||
transition-property: background-color;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.side-drawer-container-closed {
|
||||
left: -100%;
|
||||
transition-delay: 0.5s;
|
||||
transition-property: left;
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.side-drawer-click-outside {
|
||||
|
|
|
@ -145,11 +145,11 @@ const Status = {
|
|||
return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
|
||||
},
|
||||
replyToName () {
|
||||
const user = this.$store.state.users.usersObject[this.status.in_reply_to_user_id]
|
||||
if (user) {
|
||||
return user.screen_name
|
||||
} else {
|
||||
if (this.status.in_reply_to_screen_name) {
|
||||
return this.status.in_reply_to_screen_name
|
||||
} else {
|
||||
const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)
|
||||
return user && user.screen_name
|
||||
}
|
||||
},
|
||||
hideReply () {
|
||||
|
|
|
@ -23,6 +23,11 @@ const UserAvatar = {
|
|||
imageLoadError () {
|
||||
this.showPlaceholder = true
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
src () {
|
||||
this.showPlaceholder = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
@ -15,6 +16,9 @@ export default {
|
|||
betterShadow: this.$store.state.interface.browserSupport.cssFilter
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('fetchUserRelationship', this.user.id)
|
||||
},
|
||||
computed: {
|
||||
classes () {
|
||||
return [{
|
||||
|
@ -96,7 +100,8 @@ export default {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
UserAvatar
|
||||
UserAvatar,
|
||||
RemoteFollow
|
||||
},
|
||||
methods: {
|
||||
followUser () {
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
<div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
|
||||
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
|
||||
<router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser">
|
||||
<i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i>
|
||||
<i class="button-icon icon-pencil usersettings" :title="$t('tool_tip.user_settings')"></i>
|
||||
</router-link>
|
||||
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local">
|
||||
<i class="icon-link-ext usersettings"></i>
|
||||
|
@ -84,14 +84,8 @@
|
|||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="remote-follow" v-if='!loggedIn && user.is_local'>
|
||||
<form method="POST" :action='subscribeUrl'>
|
||||
<input type="hidden" name="nickname" :value="user.screen_name">
|
||||
<input type="hidden" name="profile" value="">
|
||||
<button click="submit" class="remote-button">
|
||||
{{ $t('user_card.remote_follow') }}
|
||||
</button>
|
||||
</form>
|
||||
<div v-if='!loggedIn && user.is_local'>
|
||||
<RemoteFollow :user="user" />
|
||||
</div>
|
||||
<div class='block' v-if='isOtherUser && loggedIn'>
|
||||
<span v-if='user.statusnet_blocking'>
|
||||
|
@ -159,6 +153,18 @@
|
|||
|
||||
&-bio {
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
|
||||
.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modifiers
|
||||
|
@ -363,11 +369,6 @@
|
|||
min-height: 28px;
|
||||
}
|
||||
|
||||
.remote-follow {
|
||||
max-width: 220px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.follow {
|
||||
max-width: 220px;
|
||||
min-height: 28px;
|
||||
|
|
|
@ -9,7 +9,7 @@ import withList from '../../hocs/with_list/with_list'
|
|||
const FollowerList = compose(
|
||||
withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('addFollowers', props.userId),
|
||||
select: (props, $store) => get($store.getters.userById(props.userId), 'followers', []),
|
||||
select: (props, $store) => get($store.getters.findUser(props.userId), 'followers', []),
|
||||
destory: (props, $store) => $store.dispatch('clearFollowers', props.userId),
|
||||
childPropName: 'entries',
|
||||
additionalPropNames: ['userId']
|
||||
|
@ -20,7 +20,7 @@ const FollowerList = compose(
|
|||
const FriendList = compose(
|
||||
withLoadMore({
|
||||
fetch: (props, $store) => $store.dispatch('addFriends', props.userId),
|
||||
select: (props, $store) => get($store.getters.userById(props.userId), 'friends', []),
|
||||
select: (props, $store) => get($store.getters.findUser(props.userId), 'friends', []),
|
||||
destory: (props, $store) => $store.dispatch('clearFriends', props.userId),
|
||||
childPropName: 'entries',
|
||||
additionalPropNames: ['userId']
|
||||
|
@ -31,28 +31,16 @@ const FriendList = compose(
|
|||
const UserProfile = {
|
||||
data () {
|
||||
return {
|
||||
error: false
|
||||
error: false,
|
||||
fetchedUserId: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||
this.$store.commit('clearTimeline', { timeline: 'media' })
|
||||
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
|
||||
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
|
||||
this.startFetchFavorites()
|
||||
if (!this.user.id) {
|
||||
this.$store.dispatch('fetchUser', this.fetchBy)
|
||||
.catch((reason) => {
|
||||
const errorMessage = get(reason, 'error.error')
|
||||
if (errorMessage === 'No user with such user_id') { // Known error
|
||||
this.error = this.$t('user_profile.profile_does_not_exist')
|
||||
} else if (errorMessage) {
|
||||
this.error = errorMessage
|
||||
} else {
|
||||
this.error = this.$t('user_profile.profile_loading_error')
|
||||
}
|
||||
})
|
||||
this.fetchUserId()
|
||||
.then(() => this.startUp())
|
||||
} else {
|
||||
this.startUp()
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
|
@ -69,7 +57,7 @@ const UserProfile = {
|
|||
return this.$store.state.statuses.timelines.media
|
||||
},
|
||||
userId () {
|
||||
return this.$route.params.id || this.user.id
|
||||
return this.$route.params.id || this.user.id || this.fetchedUserId
|
||||
},
|
||||
userName () {
|
||||
return this.$route.params.name || this.user.screen_name
|
||||
|
@ -79,10 +67,9 @@ const UserProfile = {
|
|||
this.userId === this.$store.state.users.currentUser.id
|
||||
},
|
||||
userInStore () {
|
||||
if (this.isExternal) {
|
||||
return this.$store.getters.userById(this.userId)
|
||||
}
|
||||
return this.$store.getters.userByName(this.userName)
|
||||
const routeParams = this.$route.params
|
||||
// This needs fetchedUserId so that computed will be refreshed when user is fetched
|
||||
return this.$store.getters.findUser(this.fetchedUserId || routeParams.name || routeParams.id)
|
||||
},
|
||||
user () {
|
||||
if (this.timeline.statuses[0]) {
|
||||
|
@ -93,9 +80,6 @@ const UserProfile = {
|
|||
}
|
||||
return {}
|
||||
},
|
||||
fetchBy () {
|
||||
return this.isExternal ? this.userId : this.userName
|
||||
},
|
||||
isExternal () {
|
||||
return this.$route.name === 'external-user-profile'
|
||||
},
|
||||
|
@ -109,14 +93,38 @@ const UserProfile = {
|
|||
methods: {
|
||||
startFetchFavorites () {
|
||||
if (this.isUs) {
|
||||
this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.fetchBy })
|
||||
this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.userId })
|
||||
}
|
||||
},
|
||||
fetchUserId () {
|
||||
let fetchPromise
|
||||
if (this.userId && !this.$route.params.name) {
|
||||
fetchPromise = this.$store.dispatch('fetchUser', this.userId)
|
||||
} else {
|
||||
fetchPromise = this.$store.dispatch('fetchUser', this.userName)
|
||||
.then(({ id }) => {
|
||||
this.fetchedUserId = id
|
||||
})
|
||||
}
|
||||
return fetchPromise
|
||||
.catch((reason) => {
|
||||
const errorMessage = get(reason, 'error.error')
|
||||
if (errorMessage === 'No user with such user_id') { // Known error
|
||||
this.error = this.$t('user_profile.profile_does_not_exist')
|
||||
} else if (errorMessage) {
|
||||
this.error = errorMessage
|
||||
} else {
|
||||
this.error = this.$t('user_profile.profile_loading_error')
|
||||
}
|
||||
})
|
||||
.then(() => this.startUp())
|
||||
},
|
||||
startUp () {
|
||||
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
|
||||
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
|
||||
|
||||
this.startFetchFavorites()
|
||||
if (this.userId) {
|
||||
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.userId })
|
||||
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.userId })
|
||||
this.startFetchFavorites()
|
||||
}
|
||||
},
|
||||
cleanUp () {
|
||||
this.$store.dispatch('stopFetching', 'user')
|
||||
|
@ -128,19 +136,19 @@ const UserProfile = {
|
|||
}
|
||||
},
|
||||
watch: {
|
||||
userName () {
|
||||
if (this.isExternal) {
|
||||
return
|
||||
// userId can be undefined if we don't know it yet
|
||||
userId (newVal) {
|
||||
if (newVal) {
|
||||
this.cleanUp()
|
||||
this.startUp()
|
||||
}
|
||||
this.cleanUp()
|
||||
this.startUp()
|
||||
},
|
||||
userId () {
|
||||
if (!this.isExternal) {
|
||||
return
|
||||
userName () {
|
||||
if (this.$route.params.name) {
|
||||
this.fetchUserId()
|
||||
this.cleanUp()
|
||||
this.startUp()
|
||||
}
|
||||
this.cleanUp()
|
||||
this.startUp()
|
||||
},
|
||||
$route () {
|
||||
this.$refs.tabSwitcher.activateTab(0)()
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
:title="$t('user_profile.timeline_title')"
|
||||
:timeline="timeline"
|
||||
:timeline-name="'user'"
|
||||
:user-id="fetchBy"
|
||||
:user-id="userId"
|
||||
/>
|
||||
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
|
||||
<FriendList :userId="userId" />
|
||||
|
@ -25,7 +25,7 @@
|
|||
:embedded="true" :title="$t('user_card.media')"
|
||||
timeline-name="media"
|
||||
:timeline="media"
|
||||
:user-id="fetchBy"
|
||||
:user-id="userId"
|
||||
/>
|
||||
<Timeline
|
||||
v-if="isUs"
|
||||
|
|
|
@ -158,7 +158,13 @@ const UserSettings = {
|
|||
reader.readAsDataURL(file)
|
||||
},
|
||||
submitAvatar (cropper, file) {
|
||||
const img = cropper.getCroppedCanvas().toDataURL(file.type)
|
||||
let img
|
||||
if (cropper) {
|
||||
img = cropper.getCroppedCanvas().toDataURL(file.type)
|
||||
} else {
|
||||
img = file
|
||||
}
|
||||
|
||||
return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => {
|
||||
if (!user.error) {
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"account_not_locked_warning_link": "مقفل",
|
||||
"attachments_sensitive": "اعتبر المرفقات كلها كمحتوى حساس",
|
||||
"content_type": {
|
||||
"plain_text": "نص صافٍ"
|
||||
"text/plain": "نص صافٍ"
|
||||
},
|
||||
"content_warning": "الموضوع (اختياري)",
|
||||
"default": "وصلت للتوّ إلى لوس أنجلس.",
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"account_not_locked_warning_link": "bloquejat",
|
||||
"attachments_sensitive": "Marca l'adjunt com a delicat",
|
||||
"content_type": {
|
||||
"plain_text": "Text pla"
|
||||
"text/plain": "Text pla"
|
||||
},
|
||||
"content_warning": "Assumpte (opcional)",
|
||||
"default": "Em sento…",
|
||||
|
|
|
@ -71,7 +71,9 @@
|
|||
"account_not_locked_warning_link": "uzamčen",
|
||||
"attachments_sensitive": "Označovat přílohy jako citlivé",
|
||||
"content_type": {
|
||||
"plain_text": "Prostý text"
|
||||
"text/plain": "Prostý text",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown"
|
||||
},
|
||||
"content_warning": "Předmět (volitelný)",
|
||||
"default": "Právě jsem přistál v L.A.",
|
||||
|
@ -95,7 +97,7 @@
|
|||
"new_captcha": "Kliknutím na obrázek získáte novou CAPTCHA",
|
||||
"username_placeholder": "např. lain",
|
||||
"fullname_placeholder": "např. Lain Iwakura",
|
||||
"bio_placeholder": "např.\nNazdar, jsem Lain\nJsem anime dívka a žiji v příměstském Japonsku. Možná mě znáte z Wired.",
|
||||
"bio_placeholder": "např.\nNazdar, jsem Lain\nJsem anime dívka žijící v příměstském Japonsku. Možná mě znáte z Wired.",
|
||||
"validations": {
|
||||
"username_required": "nemůže být prázdné",
|
||||
"fullname_required": "nemůže být prázdné",
|
||||
|
@ -204,7 +206,7 @@
|
|||
"radii_help": "Nastavit zakulacení rohů rozhraní (v pixelech)",
|
||||
"replies_in_timeline": "Odpovědi v časové ose",
|
||||
"reply_link_preview": "Povolit náhledy odkazu pro odpověď při přejetí myši",
|
||||
"reply_visibility_all": "Zobrazit všechny odpovědiShow all replies",
|
||||
"reply_visibility_all": "Zobrazit všechny odpovědi",
|
||||
"reply_visibility_following": "Zobrazit pouze odpovědi směřované na mě nebo uživatele, které sleduji",
|
||||
"reply_visibility_self": "Zobrazit pouze odpovědi směřované na mě",
|
||||
"saving_err": "Chyba při ukládání nastavení",
|
||||
|
@ -221,7 +223,6 @@
|
|||
"subject_line_mastodon": "Jako u Mastodonu: zkopírovat tak, jak je",
|
||||
"subject_line_noop": "Nekopírovat",
|
||||
"post_status_content_type": "Publikovat typ obsahu příspěvku",
|
||||
"status_content_type_plain": "Prostý text",
|
||||
"stop_gifs": "Přehrávat GIFy při přejetí myši",
|
||||
"streaming": "Povolit automatické streamování nových příspěvků při rolování nahoru",
|
||||
"text": "Text",
|
||||
|
@ -339,7 +340,7 @@
|
|||
"button": "Tlačítko",
|
||||
"text": "Spousta dalšího {0} a {1}",
|
||||
"mono": "obsahu",
|
||||
"input": "Just landed in L.A.",
|
||||
"input": "Právě jsem přistál v L.A.",
|
||||
"faint_link": "pomocný manuál",
|
||||
"fine_print": "Přečtěte si náš {0} a nenaučte se nic užitečného!",
|
||||
"header_faint": "Tohle je v pohodě",
|
||||
|
@ -361,7 +362,7 @@
|
|||
"no_statuses": "Žádné příspěvky"
|
||||
},
|
||||
"status": {
|
||||
"reply_to": "Odpovědět uživateli",
|
||||
"reply_to": "Odpověď uživateli",
|
||||
"replies_list": "Odpovědi:"
|
||||
},
|
||||
|
||||
|
@ -413,7 +414,7 @@
|
|||
"upload":{
|
||||
"error": {
|
||||
"base": "Nahrávání selhalo.",
|
||||
"file_too_big": "Soubor je úříliš velký [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||
"file_too_big": "Soubor je příliš velký [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||
"default": "Zkuste to znovu později"
|
||||
},
|
||||
"file_size_units": {
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
"account_not_locked_warning_link": "gesperrt",
|
||||
"attachments_sensitive": "Anhänge als heikel markieren",
|
||||
"content_type": {
|
||||
"plain_text": "Nur Text"
|
||||
"text/plain": "Nur Text"
|
||||
},
|
||||
"content_warning": "Betreff (optional)",
|
||||
"default": "Sitze gerade im Hofbräuhaus.",
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
"image_cropper": {
|
||||
"crop_picture": "Crop picture",
|
||||
"save": "Save",
|
||||
"save_without_cropping": "Save without cropping",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"login": {
|
||||
|
@ -347,6 +348,11 @@
|
|||
"checkbox": "I have skimmed over terms and conditions",
|
||||
"link": "a nice lil' link"
|
||||
}
|
||||
},
|
||||
"version": {
|
||||
"title": "Version",
|
||||
"backend_version": "Backend Version",
|
||||
"frontend_version": "Frontend Version"
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
"account_not_locked_warning_link": "ŝlosita",
|
||||
"attachments_sensitive": "Marki kunsendaĵojn kiel konsternajn",
|
||||
"content_type": {
|
||||
"plain_text": "Plata teksto"
|
||||
"text/plain": "Plata teksto"
|
||||
},
|
||||
"content_warning": "Temo (malnepra)",
|
||||
"default": "Ĵus alvenis al la Universala Kongreso!",
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
"account_not_locked_warning_link": "bloqueada",
|
||||
"attachments_sensitive": "Contenido sensible",
|
||||
"content_type": {
|
||||
"plain_text": "Texto Plano"
|
||||
"text/plain": "Texto Plano"
|
||||
},
|
||||
"content_warning": "Tema (opcional)",
|
||||
"default": "Acabo de aterrizar en L.A.",
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
"account_not_locked_warning_link": "lukittu",
|
||||
"attachments_sensitive": "Merkkaa liitteet arkaluonteisiksi",
|
||||
"content_type": {
|
||||
"plain_text": "Tavallinen teksti"
|
||||
"text/plain": "Tavallinen teksti"
|
||||
},
|
||||
"content_warning": "Aihe (valinnainen)",
|
||||
"default": "Tulin juuri saunasta.",
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
"account_not_locked_warning_link": "verrouillé",
|
||||
"attachments_sensitive": "Marquer le média comme sensible",
|
||||
"content_type": {
|
||||
"plain_text": "Texte brut"
|
||||
"text/plain": "Texte brut"
|
||||
},
|
||||
"content_warning": "Sujet (optionnel)",
|
||||
"default": "Écrivez ici votre prochain statut.",
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"account_not_locked_warning_link": "faoi glas",
|
||||
"attachments_sensitive": "Marcáil ceangaltán mar íogair",
|
||||
"content_type": {
|
||||
"plain_text": "Gnáth-théacs"
|
||||
"text/plain": "Gnáth-théacs"
|
||||
},
|
||||
"content_warning": "Teideal (roghnach)",
|
||||
"default": "Lá iontach anseo i nGaillimh",
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"account_not_locked_warning_link": "נעול",
|
||||
"attachments_sensitive": "סמן מסמכים מצורפים כלא בטוחים לצפייה",
|
||||
"content_type": {
|
||||
"plain_text": "טקסט פשוט"
|
||||
"text/plain": "טקסט פשוט"
|
||||
},
|
||||
"content_warning": "נושא (נתון לבחירה)",
|
||||
"default": "הרגע נחת ב-ל.א.",
|
||||
|
|
|
@ -175,7 +175,7 @@
|
|||
"account_not_locked_warning_link": "bloccato",
|
||||
"attachments_sensitive": "Segna allegati come sensibili",
|
||||
"content_type": {
|
||||
"plain_text": "Testo normale"
|
||||
"text/plain": "Testo normale"
|
||||
},
|
||||
"content_warning": "Oggetto (facoltativo)",
|
||||
"default": "Appena atterrato in L.A.",
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
"account_not_locked_warning_link": "ロックされたアカウント",
|
||||
"attachments_sensitive": "ファイルをNSFWにする",
|
||||
"content_type": {
|
||||
"plain_text": "プレーンテキスト"
|
||||
"text/plain": "プレーンテキスト"
|
||||
},
|
||||
"content_warning": "せつめい (かかなくてもよい)",
|
||||
"default": "はねだくうこうに、つきました。",
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
"account_not_locked_warning_link": "잠김",
|
||||
"attachments_sensitive": "첨부물을 민감함으로 설정",
|
||||
"content_type": {
|
||||
"plain_text": "평문"
|
||||
"text/plain": "평문"
|
||||
},
|
||||
"content_warning": "주제 (필수 아님)",
|
||||
"default": "LA에 도착!",
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"account_not_locked_warning_link": "låst",
|
||||
"attachments_sensitive": "Merk vedlegg som sensitive",
|
||||
"content_type": {
|
||||
"plain_text": "Klar tekst"
|
||||
"text/plain": "Klar tekst"
|
||||
},
|
||||
"content_warning": "Tema (valgfritt)",
|
||||
"default": "Landet akkurat i L.A.",
|
||||
|
|
|
@ -57,7 +57,7 @@
|
|||
"account_not_locked_warning_link": "gesloten",
|
||||
"attachments_sensitive": "Markeer bijlage als gevoelig",
|
||||
"content_type": {
|
||||
"plain_text": "Gewone tekst"
|
||||
"text/plain": "Gewone tekst"
|
||||
},
|
||||
"content_warning": "Onderwerp (optioneel)",
|
||||
"default": "Tijd voor een pauze!",
|
||||
|
|
147
src/i18n/oc.json
147
src/i18n/oc.json
|
@ -59,19 +59,21 @@
|
|||
"broken_favorite": "Estatut desconegut, sèm a lo cercar...",
|
||||
"favorited_you": "a aimat vòstre estatut",
|
||||
"followed_you": "vos a seguit",
|
||||
"load_older": "Cargar las notificaciones mai ancianas",
|
||||
"load_older": "Cargar las notificacions mai ancianas",
|
||||
"notifications": "Notficacions",
|
||||
"read": "Legit !",
|
||||
"read": "Legit !",
|
||||
"repeated_you": "a repetit vòstre estatut",
|
||||
"no_more_notifications": "Pas mai de notificacions"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "Publicar d’estatuts novèls",
|
||||
"account_not_locked_warning": "Vòstre compte es pas {0}. Qual que siá pòt vos seguir per veire vòstras publicacions destinadas pas qu'a vòstres seguidors.",
|
||||
"account_not_locked_warning": "Vòstre compte es pas {0}. Qual que siá pòt vos seguir per veire vòstras publicacions destinadas pas qu’a vòstres seguidors.",
|
||||
"account_not_locked_warning_link": "clavat",
|
||||
"attachments_sensitive": "Marcar las pèças juntas coma sensiblas",
|
||||
"content_type": {
|
||||
"plain_text": "Tèxte brut"
|
||||
"text/plain": "Tèxte brut",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown"
|
||||
},
|
||||
"content_warning": "Avís de contengut (opcional)",
|
||||
"default": "Escrivètz aquí vòstre estatut.",
|
||||
|
@ -118,12 +120,12 @@
|
|||
"blocks_tab": "Blocatges",
|
||||
"btnRadius": "Botons",
|
||||
"cBlue": "Blau (Respondre, seguir)",
|
||||
"cGreen": "Verd (Repartajar)",
|
||||
"cGreen": "Verd (Repertir)",
|
||||
"cOrange": "Irange (Aimar)",
|
||||
"cRed": "Roge (Anullar)",
|
||||
"change_password": "Cambiar lo senhal",
|
||||
"change_password_error": "Una error s’es producha en cambiant lo senhal.",
|
||||
"changed_password": "Senhal corrèctament cambiat !",
|
||||
"changed_password": "Senhal corrèctament cambiat !",
|
||||
"collapse_subject": "Replegar las publicacions amb de subjèctes",
|
||||
"composing": "Escritura",
|
||||
"confirm_new_password": "Confirmatz lo nòu senhal",
|
||||
|
@ -134,7 +136,7 @@
|
|||
"default_vis": "Nivèl de visibilitat per defaut",
|
||||
"delete_account": "Suprimir lo compte",
|
||||
"delete_account_description": "Suprimir vòstre compte e los messatges per sempre.",
|
||||
"delete_account_error": "Una error s’es producha en suprimir lo compte. S’aquò ten d’arribar mercés de contactar vòstre administrador d’instància.",
|
||||
"delete_account_error": "Una error s’es producha en suprimir lo compte. S’aquò ten d’arribar mercés de contactar vòstre administrator d’instància.",
|
||||
"delete_account_instructions": "Picatz vòstre senhal dins lo camp tèxte çai-jos per confirmar la supression del compte.",
|
||||
"avatar_size_instruction": "La talha minimum recomandada pels imatges d’avatar es 150x150 pixèls.",
|
||||
"export_theme": "Enregistrar la preconfiguracion",
|
||||
|
@ -154,14 +156,14 @@
|
|||
"hide_isp": "Amagar lo panèl especial instància",
|
||||
"preload_images": "Precargar los imatges",
|
||||
"use_one_click_nsfw": "Dobrir las pèças juntas NSFW amb un clic",
|
||||
"hide_post_stats": "Amagar los estatistics de publicacion (ex. lo ombre de favorits)",
|
||||
"hide_post_stats": "Amagar las estatisticas de publicacion (ex. lo nombre de favorits)",
|
||||
"hide_user_stats": "Amagar las estatisticas de l’utilizaire (ex. lo nombre de seguidors)",
|
||||
"hide_filtered_statuses": "Amagar los estatuts filtrats",
|
||||
"import_followers_from_a_csv_file": "Importar los seguidors d’un fichièr csv",
|
||||
"import_theme": "Cargar un tèma",
|
||||
"inputRadius": "Camps tèxte",
|
||||
"checkboxRadius": "Casas de marcar",
|
||||
"instance_default": "(defaut : {value})",
|
||||
"instance_default": "(defaut : {value})",
|
||||
"instance_default_simple": "(defaut)",
|
||||
"interface": "Interfàcia",
|
||||
"interfaceLanguage": "Lenga de l’interfàcia",
|
||||
|
@ -172,7 +174,7 @@
|
|||
"loop_video": "Bocla vidèo",
|
||||
"loop_video_silent_only": "Legir en bocla solament las vidèos sens son (coma los « Gifs » de Mastodon)",
|
||||
"mutes_tab": "Agamats",
|
||||
"play_videos_in_modal": "Legir las vidèoas dirèctament dins la visualizaira mèdia",
|
||||
"play_videos_in_modal": "Legir las vidèos dirèctament dins la visualizaira mèdia",
|
||||
"use_contain_fit": "Talhar pas las pèças juntas per las vinhetas",
|
||||
"name": "Nom",
|
||||
"name_bio": "Nom & Bio",
|
||||
|
@ -223,7 +225,7 @@
|
|||
"post_status_content_type": "Publicar lo tipe de contengut dels estatuts",
|
||||
"stop_gifs": "Lançar los GIFs al subrevòl",
|
||||
"streaming": "Activar lo cargament automatic dels novèls estatus en anar amont",
|
||||
"text": "Tèxt",
|
||||
"text": "Tèxte",
|
||||
"theme": "Tèma",
|
||||
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
|
||||
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
|
||||
|
@ -234,6 +236,117 @@
|
|||
"values": {
|
||||
"false": "non",
|
||||
"true": "òc"
|
||||
},
|
||||
"notifications": "Notificacions",
|
||||
"enable_web_push_notifications": "Activar las notificacions web push",
|
||||
"style": {
|
||||
"switcher": {
|
||||
"keep_color": "Gardar las colors",
|
||||
"keep_shadows": "Gardar las ombras",
|
||||
"keep_opacity": "Gardar l’opacitat",
|
||||
"keep_roundness": "Gardar la redondetat",
|
||||
"keep_fonts": "Gardar las polissas",
|
||||
"save_load_hint": "Las opcions « Gardar » permeton de servar las opcions configuradas actualament quand seleccionatz o cargatz un tèma, permeton tanben d’enregistrar aquelas opcions quand exportatz un tèma. Quand totas las casas son pas marcadas, l’exportacion de tèma o enregistrarà tot.",
|
||||
"reset": "Restablir",
|
||||
"clear_all": "O escafar tot",
|
||||
"clear_opacity": "Escafar l’opacitat"
|
||||
},
|
||||
"common": {
|
||||
"color": "Color",
|
||||
"opacity": "Opacitat",
|
||||
"contrast": {
|
||||
"hint": "Lo coeficient de contraste es de {ratio}. Dòna {level} {context}",
|
||||
"level": {
|
||||
"aa": "un nivèl AA minimum recomandat",
|
||||
"aaa": "un nivèl AAA recomandat",
|
||||
"bad": "pas un nivèl d’accessibilitat recomandat"
|
||||
},
|
||||
"context": {
|
||||
"18pt": "pel tèxte grand (18pt+)",
|
||||
"text": "pel tèxte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common_colors": {
|
||||
"_tab_label": "Comun",
|
||||
"main": "Colors comunas",
|
||||
"foreground_hint": "Vejatz « Avançat » per mai de paramètres detalhats",
|
||||
"rgbo": "Icònas, accents, badges"
|
||||
},
|
||||
"advanced_colors": {
|
||||
"_tab_label": "Avançat",
|
||||
"alert": "Rèire plan d’alèrtas",
|
||||
"alert_error": "Error",
|
||||
"badge": "Rèire plan dels badges",
|
||||
"badge_notification": "Notificacion",
|
||||
"panel_header": "Bandièra del tablèu de bòrd",
|
||||
"top_bar": "Barra amont",
|
||||
"borders": "Caires",
|
||||
"buttons": "Botons",
|
||||
"inputs": "Camps tèxte",
|
||||
"faint_text": "Tèxte descolorit"
|
||||
},
|
||||
"radii": {
|
||||
"_tab_label": "Redondetat"
|
||||
},
|
||||
"shadows": {
|
||||
"_tab_label": "Ombra e luminositat",
|
||||
"component": "Compausant",
|
||||
"override": "Subrecargar",
|
||||
"shadow_id": "Ombra #{value}",
|
||||
"blur": "Fosc",
|
||||
"spread": "Espandiment",
|
||||
"inset": "Incrustacion",
|
||||
"hint": "Per las ombras podètz tanben utilizar --variable coma valor de color per emplegar una variable CSS3. Notatz que lo paramètre d’opacitat foncionarà pas dins aquel cas.",
|
||||
"filter_hint": {
|
||||
"always_drop_shadow": "Avertiment, aquel ombra utiliza totjorn {0} quand lo navigator es compatible.",
|
||||
"drop_shadow_syntax": "{0} es pas compatible amb lo paramètre {1} e lo mot clau {2}.",
|
||||
"avatar_inset": "Notatz que combinar d’ombras incrustadas e pas incrustadas pòt donar de resultats inesperats amb los avatars transparents.",
|
||||
"spread_zero": "L’ombra amb un espandiment de > 0 apareisserà coma reglat a zèro",
|
||||
"inset_classic": "L’ombra d’incrustacion utilizarà {0}"
|
||||
},
|
||||
"components": {
|
||||
"panel": "Tablèu",
|
||||
"panelHeader": "Bandièra del tablèu",
|
||||
"topBar": "Barra amont",
|
||||
"avatar": "Utilizar l’avatar (vista perfil)",
|
||||
"avatarStatus": "Avatar de l’utilizaire (afichatge publicacion)",
|
||||
"popup": "Fenèstras sorgissentas e astúcias",
|
||||
"button": "Boton",
|
||||
"buttonHover": "Boton (en passar la mirga)",
|
||||
"buttonPressed": "Boton (en quichar)",
|
||||
"buttonPressedHover": "Boton (en quichar e passar)",
|
||||
"input": "Camp tèxte"
|
||||
}
|
||||
},
|
||||
"fonts": {
|
||||
"_tab_label": "Polissas",
|
||||
"help": "Selecionatz la polissa d’utilizar pels elements de l’UI. Per « Personalizada » vos cal picar lo nom exacte tal coma apareis sul sistèma.",
|
||||
"components": {
|
||||
"interface": "Interfàcia",
|
||||
"input": "Camps tèxte",
|
||||
"post": "Tèxte de publicacion",
|
||||
"postCode": "Tèxte Monospaced dins las publicacion (tèxte formatat)"
|
||||
},
|
||||
"family": "Nom de la polissa",
|
||||
"size": "Talha (en px)",
|
||||
"weight": "Largor (gras)",
|
||||
"custom": "Personalizada"
|
||||
},
|
||||
"preview": {
|
||||
"header": "Apercebut",
|
||||
"content": "Contengut",
|
||||
"error": "Error d’exemple",
|
||||
"button": "Boton",
|
||||
"text": "A tròç de mai de {0} e {1}",
|
||||
"mono": "contengut",
|
||||
"input": "arribada al país.",
|
||||
"faint_link": "manual d’ajuda",
|
||||
"fine_print": "Legissètz nòstre {0} per legir pas res d’util !",
|
||||
"header_faint": "Va plan",
|
||||
"checkbox": "Ai legit los tèrmes e condicions d’utilizacion",
|
||||
"link": "un pichon ligam simpatic"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
|
@ -241,19 +354,21 @@
|
|||
"conversation": "Conversacion",
|
||||
"error_fetching": "Error en cercant de mesas a jorn",
|
||||
"load_older": "Ne veire mai",
|
||||
"no_retweet_hint": "Las publicacions marcadas pels seguidors solament o dirèctas se pòdon pas repetir",
|
||||
"repeated": "repetit",
|
||||
"show_new": "Ne veire mai",
|
||||
"up_to_date": "A jorn",
|
||||
"no_retweet_hint": "La publicacion marcada coma pels seguidors solament o dirècte pòt pas èsser repetida"
|
||||
"no_more_statuses": "Pas mai d’estatuts",
|
||||
"no_statuses": "Cap d’estatuts"
|
||||
},
|
||||
"status": {
|
||||
"reply_to": "Respondre à",
|
||||
"reply_to": "Respond a",
|
||||
"replies_list": "Responsas :"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Validar",
|
||||
"block": "Blocar",
|
||||
"blocked": "Blocat !",
|
||||
"blocked": "Blocat !",
|
||||
"deny": "Refusar",
|
||||
"favorites": "Favorits",
|
||||
"follow": "Seguir",
|
||||
|
@ -263,8 +378,8 @@
|
|||
"follow_unfollow": "Quitar de seguir",
|
||||
"followees": "Abonaments",
|
||||
"followers": "Seguidors",
|
||||
"following": "Seguit !",
|
||||
"follows_you": "Vos sèc !",
|
||||
"following": "Seguit !",
|
||||
"follows_you": "Vos sèc !",
|
||||
"its_you": "Sètz vos !",
|
||||
"media": "Mèdia",
|
||||
"mute": "Amagar",
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
"public_tl": "Linha do tempo pública",
|
||||
"timeline": "Linha do tempo",
|
||||
"twkn": "Toda a rede conhecida",
|
||||
"user_search": "Busca de usuário",
|
||||
"user_search": "Buscar usuários",
|
||||
"who_to_follow": "Quem seguir",
|
||||
"preferences": "Preferências"
|
||||
},
|
||||
|
@ -67,11 +67,11 @@
|
|||
},
|
||||
"post_status": {
|
||||
"new_status": "Postar novo status",
|
||||
"account_not_locked_warning": "Sua conta não está {0}. Qualquer pessoa pode te seguir para ver seus posts restritos.",
|
||||
"account_not_locked_warning_link": "fechada",
|
||||
"account_not_locked_warning": "Sua conta não é {0}. Qualquer pessoa pode te seguir e ver seus posts privados (só para seguidores).",
|
||||
"account_not_locked_warning_link": "restrita",
|
||||
"attachments_sensitive": "Marcar anexos como sensíveis",
|
||||
"content_type": {
|
||||
"plain_text": "Texto puro"
|
||||
"text/plain": "Texto puro"
|
||||
},
|
||||
"content_warning": "Assunto (opcional)",
|
||||
"default": "Acabei de chegar no Rio!",
|
||||
|
@ -115,7 +115,7 @@
|
|||
"avatarRadius": "Avatares",
|
||||
"background": "Pano de Fundo",
|
||||
"bio": "Biografia",
|
||||
"blocks_tab": "Blocos",
|
||||
"blocks_tab": "Bloqueios",
|
||||
"btnRadius": "Botões",
|
||||
"cBlue": "Azul (Responder, seguir)",
|
||||
"cGreen": "Verde (Repetir)",
|
||||
|
@ -125,7 +125,7 @@
|
|||
"change_password_error": "Houve um erro ao modificar sua senha.",
|
||||
"changed_password": "Senha modificada com sucesso!",
|
||||
"collapse_subject": "Esconder posts com assunto",
|
||||
"composing": "Escrevendo",
|
||||
"composing": "Escrita",
|
||||
"confirm_new_password": "Confirmar nova senha",
|
||||
"current_avatar": "Seu avatar atual",
|
||||
"current_password": "Sua senha atual",
|
||||
|
@ -139,7 +139,7 @@
|
|||
"avatar_size_instruction": "O tamanho mínimo recomendado para imagens de avatar é 150x150 pixels.",
|
||||
"export_theme": "Salvar predefinições",
|
||||
"filtering": "Filtragem",
|
||||
"filtering_explanation": "Todas as postagens contendo estas palavras serão silenciadas, uma por linha.",
|
||||
"filtering_explanation": "Todas as postagens contendo estas palavras serão silenciadas; uma palavra por linha.",
|
||||
"follow_export": "Exportar quem você segue",
|
||||
"follow_export_button": "Exportar quem você segue para um arquivo CSV",
|
||||
"follow_export_processing": "Processando. Em breve você receberá a solicitação de download do arquivo",
|
||||
|
@ -178,7 +178,7 @@
|
|||
"name_bio": "Nome & Biografia",
|
||||
"new_password": "Nova senha",
|
||||
"notification_visibility": "Tipos de notificação para mostrar",
|
||||
"notification_visibility_follows": "Seguidos",
|
||||
"notification_visibility_follows": "Seguidas",
|
||||
"notification_visibility_likes": "Favoritos",
|
||||
"notification_visibility_mentions": "Menções",
|
||||
"notification_visibility_repeats": "Repetições",
|
||||
|
@ -187,7 +187,7 @@
|
|||
"no_mutes": "Sem silenciados",
|
||||
"hide_follows_description": "Não mostrar quem estou seguindo",
|
||||
"hide_followers_description": "Não mostrar quem me segue",
|
||||
"show_admin_badge": "Mostrar distintivo de Administrador em meu perfil",
|
||||
"show_admin_badge": "Mostrar título de Administrador em meu perfil",
|
||||
"show_moderator_badge": "Mostrar título de Moderador em meu perfil",
|
||||
"nsfw_clickthrough": "Habilitar clique para ocultar anexos sensíveis",
|
||||
"oauth_tokens": "Token OAuth",
|
||||
|
@ -201,9 +201,9 @@
|
|||
"profile_background": "Pano de fundo de perfil",
|
||||
"profile_banner": "Capa de perfil",
|
||||
"profile_tab": "Perfil",
|
||||
"radii_help": "Arredondar arestas da interface (em píxeis)",
|
||||
"radii_help": "Arredondar arestas da interface (em pixel)",
|
||||
"replies_in_timeline": "Respostas na linha do tempo",
|
||||
"reply_link_preview": "Habilitar a pré-visualização de link de respostas ao passar o mouse.",
|
||||
"reply_link_preview": "Habilitar a pré-visualização de de respostas ao passar o mouse.",
|
||||
"reply_visibility_all": "Mostrar todas as respostas",
|
||||
"reply_visibility_following": "Só mostrar respostas direcionadas a mim ou a usuários que sigo",
|
||||
"reply_visibility_self": "Só mostrar respostas direcionadas a mim",
|
||||
|
@ -212,7 +212,7 @@
|
|||
"security_tab": "Segurança",
|
||||
"scope_copy": "Copiar opções de privacidade ao responder (Mensagens diretas sempre copiam)",
|
||||
"set_new_avatar": "Alterar avatar",
|
||||
"set_new_profile_background": "Alterar o plano de fundo de perfil",
|
||||
"set_new_profile_background": "Alterar o pano de fundo de perfil",
|
||||
"set_new_profile_banner": "Alterar capa de perfil",
|
||||
"settings": "Configurações",
|
||||
"subject_input_always_show": "Sempre mostrar campo de assunto",
|
||||
|
@ -220,9 +220,9 @@
|
|||
"subject_line_email": "Como em email: \"re: assunto\"",
|
||||
"subject_line_mastodon": "Como o Mastodon: copiar como está",
|
||||
"subject_line_noop": "Não copiar",
|
||||
"post_status_content_type": "Postar tipo de conteúdo do status",
|
||||
"stop_gifs": "Reproduzir GIFs ao passar o cursor em cima",
|
||||
"streaming": "Habilitar o fluxo automático de postagens quando ao topo da página",
|
||||
"post_status_content_type": "Tipo de conteúdo do status",
|
||||
"stop_gifs": "Reproduzir GIFs ao passar o cursor",
|
||||
"streaming": "Habilitar o fluxo automático de postagens no topo da página",
|
||||
"text": "Texto",
|
||||
"theme": "Tema",
|
||||
"theme_help": "Use cores em código hexadecimal (#rrggbb) para personalizar seu esquema de cores.",
|
||||
|
@ -235,7 +235,7 @@
|
|||
"false": "não",
|
||||
"true": "sim"
|
||||
},
|
||||
"notifications": "Notifications",
|
||||
"notifications": "Notificações",
|
||||
"enable_web_push_notifications": "Habilitar notificações web push",
|
||||
"style": {
|
||||
"switcher": {
|
||||
|
@ -245,7 +245,7 @@
|
|||
"keep_roundness": "Manter arredondado",
|
||||
"keep_fonts": "Manter fontes",
|
||||
"save_load_hint": "Manter as opções preserva as opções atuais ao selecionar ou carregar temas; também salva as opções ao exportar um tempo. Quanto todos os campos estiverem desmarcados, tudo será salvo ao exportar o tema.",
|
||||
"reset": "Voltar ao padrão",
|
||||
"reset": "Restaurar o padrão",
|
||||
"clear_all": "Limpar tudo",
|
||||
"clear_opacity": "Limpar opacidade"
|
||||
},
|
||||
|
@ -319,7 +319,7 @@
|
|||
},
|
||||
"fonts": {
|
||||
"_tab_label": "Fontes",
|
||||
"help": "Selecionar fonte dos elementos da interface. Para fonte \"personalizada\" você deve entrar exatamente o nome da fonte no sistema.",
|
||||
"help": "Selecione as fontes dos elementos da interface. Para fonte \"personalizada\" você deve inserir o mesmo nome da fonte no sistema.",
|
||||
"components": {
|
||||
"interface": "Interface",
|
||||
"input": "Campo de entrada",
|
||||
|
@ -383,7 +383,7 @@
|
|||
"mute": "Silenciar",
|
||||
"muted": "Silenciado",
|
||||
"per_day": "por dia",
|
||||
"remote_follow": "Seguidor Remoto",
|
||||
"remote_follow": "Seguir remotamente",
|
||||
"statuses": "Postagens",
|
||||
"unblock": "Desbloquear",
|
||||
"unblock_progress": "Desbloqueando...",
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"account_not_locked_warning_link": "上锁",
|
||||
"attachments_sensitive": "标记附件为敏感内容",
|
||||
"content_type": {
|
||||
"plain_text": "纯文本"
|
||||
"text/plain": "纯文本"
|
||||
},
|
||||
"content_warning": "主题(可选)",
|
||||
"default": "刚刚抵达上海",
|
||||
|
|
|
@ -60,18 +60,6 @@ export default function createPersistedState ({
|
|||
merge({}, store.state, savedState)
|
||||
)
|
||||
}
|
||||
if (store.state.config.customTheme) {
|
||||
// This is a hack to deal with async loading of config.json and themes
|
||||
// See: style_setter.js, setPreset()
|
||||
window.themeLoaded = true
|
||||
store.dispatch('setOption', {
|
||||
name: 'customTheme',
|
||||
value: store.state.config.customTheme
|
||||
})
|
||||
}
|
||||
if (store.state.oauth.token) {
|
||||
store.dispatch('loginUser', store.state.oauth.token)
|
||||
}
|
||||
loaded = true
|
||||
} catch (e) {
|
||||
console.log("Couldn't load state")
|
||||
|
|
|
@ -53,9 +53,10 @@ const persistedStateOptions = {
|
|||
'users.lastLoginName',
|
||||
'oauth'
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
createPersistedState(persistedStateOptions).then((persistedState) => {
|
||||
(async () => {
|
||||
const persistedState = await createPersistedState(persistedStateOptions)
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
interface: interfaceModule,
|
||||
|
@ -75,7 +76,7 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
|
|||
})
|
||||
|
||||
afterStoreSetup({ store, i18n })
|
||||
})
|
||||
})()
|
||||
|
||||
// These are inlined by webpack's DefinePlugin
|
||||
/* eslint-disable */
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
const chat = {
|
||||
state: {
|
||||
messages: [],
|
||||
channel: {state: ''}
|
||||
channel: {state: ''},
|
||||
socket: null
|
||||
},
|
||||
mutations: {
|
||||
setChannel (state, channel) {
|
||||
state.channel = channel
|
||||
},
|
||||
setSocket (state, socket) {
|
||||
state.socket = socket
|
||||
},
|
||||
addMessage (state, message) {
|
||||
state.messages.push(message)
|
||||
state.messages = state.messages.slice(-19, 20)
|
||||
|
@ -16,8 +20,12 @@ const chat = {
|
|||
}
|
||||
},
|
||||
actions: {
|
||||
disconnectFromChat (store) {
|
||||
store.state.socket.disconnect()
|
||||
},
|
||||
initializeChat (store, socket) {
|
||||
const channel = socket.channel('chat:public')
|
||||
store.commit('setSocket', socket)
|
||||
channel.on('new_msg', (msg) => {
|
||||
store.commit('addMessage', msg)
|
||||
})
|
||||
|
|
|
@ -48,7 +48,11 @@ const defaultState = {
|
|||
|
||||
// Html stuff
|
||||
instanceSpecificPanelContent: '',
|
||||
tos: ''
|
||||
tos: '',
|
||||
|
||||
// Version Information
|
||||
backendVersion: '',
|
||||
frontendVersion: ''
|
||||
}
|
||||
|
||||
const instance = {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { remove, slice, each, find, maxBy, minBy, merge, first, last, isArray } from 'lodash'
|
||||
import { remove, slice, each, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash'
|
||||
import apiService from '../services/api/api.service.js'
|
||||
// import parse from '../services/status_parser/status_parser.js'
|
||||
|
||||
|
@ -72,7 +72,9 @@ const mergeOrAdd = (arr, obj, item) => {
|
|||
|
||||
if (oldItem) {
|
||||
// We already have this, so only merge the new info.
|
||||
merge(oldItem, item)
|
||||
// We ignore null values to avoid overwriting existing properties with missing data
|
||||
// we also skip 'user' because that is handled by users module
|
||||
merge(oldItem, omitBy(item, (v, k) => v === null || k === 'user'))
|
||||
// Reactivity fix.
|
||||
oldItem.attachments.splice(oldItem.attachments.length)
|
||||
return {item: oldItem, new: false}
|
||||
|
@ -333,6 +335,7 @@ export const mutations = {
|
|||
oldTimeline.newStatusCount = 0
|
||||
oldTimeline.visibleStatuses = slice(oldTimeline.statuses, 0, 50)
|
||||
oldTimeline.minVisibleId = last(oldTimeline.visibleStatuses).id
|
||||
oldTimeline.minId = oldTimeline.minVisibleId
|
||||
oldTimeline.visibleStatusesObject = {}
|
||||
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
|
||||
},
|
||||
|
|
|
@ -18,7 +18,7 @@ export const mergeOrAdd = (arr, obj, item) => {
|
|||
arr.push(item)
|
||||
obj[item.id] = item
|
||||
if (item.screen_name && !item.screen_name.includes('@')) {
|
||||
obj[item.screen_name] = item
|
||||
obj[item.screen_name.toLowerCase()] = item
|
||||
}
|
||||
return { item, new: true }
|
||||
}
|
||||
|
@ -91,6 +91,17 @@ export const mutations = {
|
|||
addNewUsers (state, users) {
|
||||
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
|
||||
},
|
||||
updateUserRelationship (state, relationships) {
|
||||
relationships.forEach((relationship) => {
|
||||
const user = state.usersObject[relationship.id]
|
||||
if (user) {
|
||||
user.follows_you = relationship.followed_by
|
||||
user.following = relationship.following
|
||||
user.muted = relationship.muting
|
||||
user.statusnet_blocking = relationship.blocking
|
||||
}
|
||||
})
|
||||
},
|
||||
saveBlocks (state, blockIds) {
|
||||
state.currentUser.blockIds = blockIds
|
||||
},
|
||||
|
@ -122,12 +133,14 @@ export const mutations = {
|
|||
}
|
||||
|
||||
export const getters = {
|
||||
userById: state => id =>
|
||||
state.users.find(user => user.id === id),
|
||||
userByName: state => name =>
|
||||
state.users.find(user => user.screen_name &&
|
||||
(user.screen_name.toLowerCase() === name.toLowerCase())
|
||||
)
|
||||
findUser: state => query => {
|
||||
const result = state.usersObject[query]
|
||||
// In case it's a screen_name, we can try searching case-insensitive
|
||||
if (!result && typeof query === 'string') {
|
||||
return state.usersObject[query.toLowerCase()]
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultState = {
|
||||
|
@ -147,7 +160,14 @@ const users = {
|
|||
actions: {
|
||||
fetchUser (store, id) {
|
||||
return store.rootState.api.backendInteractor.fetchUser({ id })
|
||||
.then((user) => store.commit('addNewUsers', [user]))
|
||||
.then((user) => {
|
||||
store.commit('addNewUsers', [user])
|
||||
return user
|
||||
})
|
||||
},
|
||||
fetchUserRelationship (store, id) {
|
||||
return store.rootState.api.backendInteractor.fetchUserRelationship({ id })
|
||||
.then((relationships) => store.commit('updateUserRelationship', relationships))
|
||||
},
|
||||
fetchBlocks (store) {
|
||||
return store.rootState.api.backendInteractor.fetchBlocks()
|
||||
|
@ -292,6 +312,7 @@ const users = {
|
|||
|
||||
logout (store) {
|
||||
store.commit('clearCurrentUser')
|
||||
store.dispatch('disconnectFromChat')
|
||||
store.commit('setToken', false)
|
||||
store.dispatch('stopFetching', 'friends')
|
||||
store.commit('setBackendInteractor', backendInteractorService())
|
||||
|
@ -321,6 +342,9 @@ const users = {
|
|||
|
||||
if (user.token) {
|
||||
store.dispatch('setWsToken', user.token)
|
||||
|
||||
// Initialize the chat socket.
|
||||
store.dispatch('initializeSocket')
|
||||
}
|
||||
|
||||
// Start getting fresh posts.
|
||||
|
|
|
@ -27,12 +27,10 @@ const BG_UPDATE_URL = '/api/qvitter/update_background_image.json'
|
|||
const BANNER_UPDATE_URL = '/api/account/update_profile_banner.json'
|
||||
const PROFILE_UPDATE_URL = '/api/account/update_profile.json'
|
||||
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
|
||||
const QVITTER_USER_TIMELINE_URL = '/api/qvitter/statuses/user_timeline.json'
|
||||
const QVITTER_USER_NOTIFICATIONS_URL = '/api/qvitter/statuses/notifications.json'
|
||||
const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
|
||||
const BLOCKING_URL = '/api/blocks/create.json'
|
||||
const UNBLOCKING_URL = '/api/blocks/destroy.json'
|
||||
const USER_URL = '/api/users/show.json'
|
||||
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
|
||||
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
|
||||
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
|
||||
|
@ -43,6 +41,9 @@ const SUGGESTIONS_URL = '/api/v1/suggestions'
|
|||
|
||||
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
|
||||
const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = '/api/v1/timelines/direct'
|
||||
const MASTODON_USER_URL = '/api/v1/accounts'
|
||||
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
|
||||
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
|
||||
|
||||
import { each, map } from 'lodash'
|
||||
import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
|
@ -243,7 +244,7 @@ const denyUser = ({id, credentials}) => {
|
|||
}
|
||||
|
||||
const fetchUser = ({id, credentials}) => {
|
||||
let url = `${USER_URL}?user_id=${id}`
|
||||
let url = `${MASTODON_USER_URL}/${id}`
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((response) => {
|
||||
return new Promise((resolve, reject) => response.json()
|
||||
|
@ -257,6 +258,20 @@ const fetchUser = ({id, credentials}) => {
|
|||
.then((data) => parseUser(data))
|
||||
}
|
||||
|
||||
const fetchUserRelationship = ({id, credentials}) => {
|
||||
let url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}`
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((response) => {
|
||||
return new Promise((resolve, reject) => response.json()
|
||||
.then((json) => {
|
||||
if (!response.ok) {
|
||||
return reject(new StatusCodeError(response.status, json, { url }, response))
|
||||
}
|
||||
return resolve(json)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
const fetchFriends = ({id, page, credentials}) => {
|
||||
let url = `${FRIENDS_URL}?user_id=${id}`
|
||||
if (page) {
|
||||
|
@ -347,8 +362,8 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
|
|||
dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
|
||||
notifications: QVITTER_USER_NOTIFICATIONS_URL,
|
||||
'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL,
|
||||
user: QVITTER_USER_TIMELINE_URL,
|
||||
media: QVITTER_USER_TIMELINE_URL,
|
||||
user: MASTODON_USER_TIMELINE_URL,
|
||||
media: MASTODON_USER_TIMELINE_URL,
|
||||
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
|
||||
tag: TAG_TIMELINE_URL
|
||||
}
|
||||
|
@ -357,15 +372,16 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
|
|||
|
||||
let url = timelineUrls[timeline]
|
||||
|
||||
if (timeline === 'user' || timeline === 'media') {
|
||||
url = url(userId)
|
||||
}
|
||||
|
||||
if (since) {
|
||||
params.push(['since_id', since])
|
||||
}
|
||||
if (until) {
|
||||
params.push(['max_id', until])
|
||||
}
|
||||
if (userId) {
|
||||
params.push(['user_id', userId])
|
||||
}
|
||||
if (tag) {
|
||||
url += `/${tag}.json`
|
||||
}
|
||||
|
@ -545,7 +561,12 @@ const fetchOAuthTokens = ({credentials}) => {
|
|||
|
||||
return fetch(url, {
|
||||
headers: authHeaders(credentials)
|
||||
}).then((data) => data.json())
|
||||
}).then((data) => {
|
||||
if (data.ok) {
|
||||
return data.json()
|
||||
}
|
||||
throw new Error('Error fetching auth tokens', data)
|
||||
})
|
||||
}
|
||||
|
||||
const revokeOAuthToken = ({id, credentials}) => {
|
||||
|
@ -588,6 +609,7 @@ const apiService = {
|
|||
blockUser,
|
||||
unblockUser,
|
||||
fetchUser,
|
||||
fetchUserRelationship,
|
||||
favorite,
|
||||
unfavorite,
|
||||
retweet,
|
||||
|
|
|
@ -30,6 +30,10 @@ const backendInteractorService = (credentials) => {
|
|||
return apiService.fetchUser({id, credentials})
|
||||
}
|
||||
|
||||
const fetchUserRelationship = ({id}) => {
|
||||
return apiService.fetchUserRelationship({id, credentials})
|
||||
}
|
||||
|
||||
const followUser = (id) => {
|
||||
return apiService.followUser({credentials, id})
|
||||
}
|
||||
|
@ -92,6 +96,7 @@ const backendInteractorService = (credentials) => {
|
|||
blockUser,
|
||||
unblockUser,
|
||||
fetchUser,
|
||||
fetchUserRelationship,
|
||||
fetchAllFollowing,
|
||||
verifyCredentials: apiService.verifyCredentials,
|
||||
startFetching,
|
||||
|
|
|
@ -39,11 +39,11 @@ export const parseUser = (data) => {
|
|||
return output
|
||||
}
|
||||
|
||||
output.name = null // missing
|
||||
output.name_html = data.display_name
|
||||
// output.name = ??? missing
|
||||
output.name_html = addEmojis(data.display_name, data.emojis)
|
||||
|
||||
output.description = null // missing
|
||||
output.description_html = data.note
|
||||
// output.description = ??? missing
|
||||
output.description_html = addEmojis(data.note, data.emojis)
|
||||
|
||||
// Utilize avatar_static for gif avatars?
|
||||
output.profile_image_url = data.avatar
|
||||
|
@ -59,10 +59,14 @@ export const parseUser = (data) => {
|
|||
output.statusnet_profile_url = data.url
|
||||
|
||||
if (data.pleroma) {
|
||||
const pleroma = data.pleroma
|
||||
output.follows_you = pleroma.follows_you
|
||||
output.statusnet_blocking = pleroma.statusnet_blocking
|
||||
output.muted = pleroma.muted
|
||||
const relationship = data.pleroma.relationship
|
||||
|
||||
if (relationship) {
|
||||
output.follows_you = relationship.followed_by
|
||||
output.following = relationship.following
|
||||
output.statusnet_blocking = relationship.blocking
|
||||
output.muted = relationship.muting
|
||||
}
|
||||
}
|
||||
|
||||
// Missing, trying to recover
|
||||
|
@ -83,7 +87,7 @@ export const parseUser = (data) => {
|
|||
|
||||
output.friends_count = data.friends_count
|
||||
|
||||
output.bot = null // missing
|
||||
// output.bot = ??? missing
|
||||
|
||||
output.statusnet_profile_url = data.statusnet_profile_url
|
||||
|
||||
|
@ -134,7 +138,7 @@ const parseAttachment = (data) => {
|
|||
output.meta = data.meta // not present in BE yet
|
||||
} else {
|
||||
output.mimetype = data.mimetype
|
||||
output.meta = null // missing
|
||||
// output.meta = ??? missing
|
||||
}
|
||||
|
||||
output.url = data.url
|
||||
|
@ -142,6 +146,14 @@ const parseAttachment = (data) => {
|
|||
|
||||
return output
|
||||
}
|
||||
export const addEmojis = (string, emojis) => {
|
||||
return emojis.reduce((acc, emoji) => {
|
||||
return acc.replace(
|
||||
new RegExp(`:${emoji.shortcode}:`, 'g'),
|
||||
`<img src='${emoji.url}' alt='${emoji.shortcode}' class='emoji' />`
|
||||
)
|
||||
}, string)
|
||||
}
|
||||
|
||||
export const parseStatus = (data) => {
|
||||
const output = {}
|
||||
|
@ -157,7 +169,7 @@ export const parseStatus = (data) => {
|
|||
output.type = data.reblog ? 'retweet' : 'status'
|
||||
output.nsfw = data.sensitive
|
||||
|
||||
output.statusnet_html = data.content
|
||||
output.statusnet_html = addEmojis(data.content, data.emojis)
|
||||
|
||||
// Not exactly the same but works?
|
||||
output.text = data.content
|
||||
|
@ -166,7 +178,7 @@ export const parseStatus = (data) => {
|
|||
output.in_reply_to_user_id = data.in_reply_to_account_id
|
||||
|
||||
// Missing!! fix in UI?
|
||||
output.in_reply_to_screen_name = null
|
||||
// output.in_reply_to_screen_name = ???
|
||||
|
||||
// Not exactly the same but works
|
||||
output.statusnet_conversation_id = data.id
|
||||
|
@ -176,11 +188,10 @@ export const parseStatus = (data) => {
|
|||
}
|
||||
|
||||
output.summary = data.spoiler_text
|
||||
output.summary_html = data.spoiler_text
|
||||
output.summary_html = addEmojis(data.spoiler_text, data.emojis)
|
||||
output.external_url = data.url
|
||||
|
||||
// FIXME missing!!
|
||||
output.is_local = false
|
||||
// output.is_local = ??? missing
|
||||
} else {
|
||||
output.favorited = data.favorited
|
||||
output.fave_num = data.fave_num
|
||||
|
@ -259,7 +270,7 @@ export const parseNotification = (data) => {
|
|||
|
||||
if (masto) {
|
||||
output.type = mastoDict[data.type] || data.type
|
||||
output.seen = null // missing
|
||||
// output.seen = ??? missing
|
||||
output.status = parseStatus(data.status)
|
||||
output.action = output.status // not sure
|
||||
output.from_profile = parseUser(data.account)
|
||||
|
|
6
src/services/version/version.service.js
Normal file
6
src/services/version/version.service.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
export const extractCommit = versionString => {
|
||||
const regex = /-g(\w+)$/i
|
||||
const matches = versionString.match(regex)
|
||||
return matches ? matches[1] : ''
|
||||
}
|
0
static/font/LICENSE.txt
Normal file → Executable file
0
static/font/LICENSE.txt
Normal file → Executable file
0
static/font/README.txt
Normal file → Executable file
0
static/font/README.txt
Normal file → Executable file
6
static/font/config.json
Normal file → Executable file
6
static/font/config.json
Normal file → Executable file
|
@ -233,6 +233,12 @@
|
|||
"css": "play-circled",
|
||||
"code": 61764,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "d35a1d35efeb784d1dc9ac18b9b6c2b6",
|
||||
"css": "pencil",
|
||||
"code": 59416,
|
||||
"src": "fontawesome"
|
||||
}
|
||||
]
|
||||
}
|
0
static/font/css/animation.css
Normal file → Executable file
0
static/font/css/animation.css
Normal file → Executable file
1
static/font/css/fontello-codes.css
vendored
Normal file → Executable file
1
static/font/css/fontello-codes.css
vendored
Normal file → Executable file
|
@ -23,6 +23,7 @@
|
|||
.icon-plus:before { content: '\e815'; } /* '' */
|
||||
.icon-adjust:before { content: '\e816'; } /* '' */
|
||||
.icon-edit:before { content: '\e817'; } /* '' */
|
||||
.icon-pencil:before { content: '\e818'; } /* '' */
|
||||
.icon-spin3:before { content: '\e832'; } /* '' */
|
||||
.icon-spin4:before { content: '\e834'; } /* '' */
|
||||
.icon-link-ext:before { content: '\f08e'; } /* '' */
|
||||
|
|
13
static/font/css/fontello-embedded.css
vendored
Normal file → Executable file
13
static/font/css/fontello-embedded.css
vendored
Normal file → Executable file
File diff suppressed because one or more lines are too long
1
static/font/css/fontello-ie7-codes.css
vendored
Normal file → Executable file
1
static/font/css/fontello-ie7-codes.css
vendored
Normal file → Executable file
|
@ -23,6 +23,7 @@
|
|||
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
1
static/font/css/fontello-ie7.css
vendored
Normal file → Executable file
1
static/font/css/fontello-ie7.css
vendored
Normal file → Executable file
|
@ -34,6 +34,7 @@
|
|||
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
15
static/font/css/fontello.css
vendored
Normal file → Executable file
15
static/font/css/fontello.css
vendored
Normal file → Executable file
|
@ -1,11 +1,11 @@
|
|||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('../font/fontello.eot?94672585');
|
||||
src: url('../font/fontello.eot?94672585#iefix') format('embedded-opentype'),
|
||||
url('../font/fontello.woff2?94672585') format('woff2'),
|
||||
url('../font/fontello.woff?94672585') format('woff'),
|
||||
url('../font/fontello.ttf?94672585') format('truetype'),
|
||||
url('../font/fontello.svg?94672585#fontello') format('svg');
|
||||
src: url('../font/fontello.eot?40679575');
|
||||
src: url('../font/fontello.eot?40679575#iefix') format('embedded-opentype'),
|
||||
url('../font/fontello.woff2?40679575') format('woff2'),
|
||||
url('../font/fontello.woff?40679575') format('woff'),
|
||||
url('../font/fontello.ttf?40679575') format('truetype'),
|
||||
url('../font/fontello.svg?40679575#fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
|||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('../font/fontello.svg?94672585#fontello') format('svg');
|
||||
src: url('../font/fontello.svg?40679575#fontello') format('svg');
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@ -79,6 +79,7 @@
|
|||
.icon-plus:before { content: '\e815'; } /* '' */
|
||||
.icon-adjust:before { content: '\e816'; } /* '' */
|
||||
.icon-edit:before { content: '\e817'; } /* '' */
|
||||
.icon-pencil:before { content: '\e818'; } /* '' */
|
||||
.icon-spin3:before { content: '\e832'; } /* '' */
|
||||
.icon-spin4:before { content: '\e834'; } /* '' */
|
||||
.icon-link-ext:before { content: '\f08e'; } /* '' */
|
||||
|
|
17
static/font/demo.html
Normal file → Executable file
17
static/font/demo.html
Normal file → Executable file
|
@ -229,11 +229,11 @@ body {
|
|||
}
|
||||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('./font/fontello.eot?28736547');
|
||||
src: url('./font/fontello.eot?28736547#iefix') format('embedded-opentype'),
|
||||
url('./font/fontello.woff?28736547') format('woff'),
|
||||
url('./font/fontello.ttf?28736547') format('truetype'),
|
||||
url('./font/fontello.svg?28736547#fontello') format('svg');
|
||||
src: url('./font/fontello.eot?50378338');
|
||||
src: url('./font/fontello.eot?50378338#iefix') format('embedded-opentype'),
|
||||
url('./font/fontello.woff?50378338') format('woff'),
|
||||
url('./font/fontello.ttf?50378338') format('truetype'),
|
||||
url('./font/fontello.svg?50378338#fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -334,24 +334,25 @@ body {
|
|||
<div class="the-icons span3" title="Code: 0xe817"><i class="demo-icon icon-edit"></i> <span class="i-name">icon-edit</span><span class="i-code">0xe817</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xe818"><i class="demo-icon icon-pencil"></i> <span class="i-name">icon-pencil</span><span class="i-code">0xe818</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin"></i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin"></i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext"></i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt"></i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt"></i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu"></i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt"></i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty"></i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared"></i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared"></i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply"></i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt"></i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf144"><i class="demo-icon icon-play-circled"></i> <span class="i-name">icon-play-circled</span><span class="i-code">0xf144</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt"></i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt"></i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars"></i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus"></i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div>
|
||||
</div>
|
||||
|
|
BIN
static/font/font/fontello.eot
Normal file → Executable file
BIN
static/font/font/fontello.eot
Normal file → Executable file
Binary file not shown.
2
static/font/font/fontello.svg
Normal file → Executable file
2
static/font/font/fontello.svg
Normal file → Executable file
|
@ -54,6 +54,8 @@
|
|||
|
||||
<glyph glyph-name="edit" unicode="" d="M496 196l64 65-85 85-64-65v-31h53v-54h32z m245 402q-9 9-18 0l-196-196q-9-9 0-18t18 0l196 196q9 9 0 18z m45-331v-106q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-8-8-18-4-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v70q0 7 5 12l36 36q8 8 20 4t11-16z m-54 411l161-160-375-375h-161v160z m248-73l-51-52-161 161 51 52q16 15 38 15t38-15l85-85q16-16 16-38t-16-38z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="pencil" unicode="" d="M203 0l50 51-131 131-51-51v-60h72v-71h60z m291 518q0 12-12 12-5 0-9-4l-303-302q-4-4-4-10 0-12 13-12 5 0 9 4l303 302q3 4 3 10z m-30 107l232-232-464-465h-232v233z m381-54q0-29-20-50l-93-93-232 233 93 92q20 21 50 21 29 0 51-21l131-131q20-22 20-51z" horiz-adv-x="857.1" />
|
||||
|
||||
<glyph glyph-name="spin3" unicode="" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="spin4" unicode="" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
|
||||
|
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
BIN
static/font/font/fontello.ttf
Normal file → Executable file
BIN
static/font/font/fontello.ttf
Normal file → Executable file
Binary file not shown.
BIN
static/font/font/fontello.woff
Normal file → Executable file
BIN
static/font/font/fontello.woff
Normal file → Executable file
Binary file not shown.
BIN
static/font/font/fontello.woff2
Normal file → Executable file
BIN
static/font/font/fontello.woff2
Normal file → Executable file
Binary file not shown.
|
@ -12,9 +12,13 @@ const mutations = {
|
|||
setError: () => {}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
fetchUser: () => {},
|
||||
fetchUserByScreenName: () => {}
|
||||
}
|
||||
|
||||
const testGetters = {
|
||||
userByName: state => getters.userByName(state.users),
|
||||
userById: state => getters.userById(state.users)
|
||||
findUser: state => getters.findUser(state.users)
|
||||
}
|
||||
|
||||
const localUser = {
|
||||
|
@ -31,6 +35,7 @@ const extUser = {
|
|||
|
||||
const externalProfileStore = new Vuex.Store({
|
||||
mutations,
|
||||
actions,
|
||||
getters: testGetters,
|
||||
state: {
|
||||
api: {
|
||||
|
@ -89,7 +94,7 @@ const externalProfileStore = new Vuex.Store({
|
|||
currentUser: {
|
||||
credentials: ''
|
||||
},
|
||||
usersObject: [extUser],
|
||||
usersObject: { 100: extUser },
|
||||
users: [extUser]
|
||||
}
|
||||
}
|
||||
|
@ -97,6 +102,7 @@ const externalProfileStore = new Vuex.Store({
|
|||
|
||||
const localProfileStore = new Vuex.Store({
|
||||
mutations,
|
||||
actions,
|
||||
getters: testGetters,
|
||||
state: {
|
||||
api: {
|
||||
|
@ -155,7 +161,7 @@ const localProfileStore = new Vuex.Store({
|
|||
currentUser: {
|
||||
credentials: ''
|
||||
},
|
||||
usersObject: [localUser],
|
||||
usersObject: { 100: localUser, 'testuser': localUser },
|
||||
users: [localUser]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,238 +14,258 @@ const makeMockStatus = ({id, text, type = 'status'}) => {
|
|||
}
|
||||
}
|
||||
|
||||
describe('Statuses.prepareStatus', () => {
|
||||
it('sets deleted flag to false', () => {
|
||||
const aStatus = makeMockStatus({id: '1', text: 'Hello oniichan'})
|
||||
expect(prepareStatus(aStatus).deleted).to.eq(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('The Statuses module', () => {
|
||||
it('adds the status to allStatuses and to the given timeline', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' })
|
||||
|
||||
expect(state.allStatuses).to.eql([status])
|
||||
expect(state.timelines.public.statuses).to.eql([status])
|
||||
expect(state.timelines.public.visibleStatuses).to.eql([])
|
||||
expect(state.timelines.public.newStatusCount).to.equal(1)
|
||||
describe('Statuses module', () => {
|
||||
describe('prepareStatus', () => {
|
||||
it('sets deleted flag to false', () => {
|
||||
const aStatus = makeMockStatus({id: '1', text: 'Hello oniichan'})
|
||||
expect(prepareStatus(aStatus).deleted).to.eq(false)
|
||||
})
|
||||
})
|
||||
|
||||
it('counts the status as new if it has not been seen on this timeline', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
describe('addNewStatuses', () => {
|
||||
it('adds the status to allStatuses and to the given timeline', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' })
|
||||
mutations.addNewStatuses(state, { statuses: [status], timeline: 'friends' })
|
||||
mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' })
|
||||
|
||||
expect(state.allStatuses).to.eql([status])
|
||||
expect(state.timelines.public.statuses).to.eql([status])
|
||||
expect(state.timelines.public.visibleStatuses).to.eql([])
|
||||
expect(state.timelines.public.newStatusCount).to.equal(1)
|
||||
expect(state.allStatuses).to.eql([status])
|
||||
expect(state.timelines.public.statuses).to.eql([status])
|
||||
expect(state.timelines.public.visibleStatuses).to.eql([])
|
||||
expect(state.timelines.public.newStatusCount).to.equal(1)
|
||||
})
|
||||
|
||||
expect(state.allStatuses).to.eql([status])
|
||||
expect(state.timelines.friends.statuses).to.eql([status])
|
||||
expect(state.timelines.friends.visibleStatuses).to.eql([])
|
||||
expect(state.timelines.friends.newStatusCount).to.equal(1)
|
||||
it('counts the status as new if it has not been seen on this timeline', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' })
|
||||
mutations.addNewStatuses(state, { statuses: [status], timeline: 'friends' })
|
||||
|
||||
expect(state.allStatuses).to.eql([status])
|
||||
expect(state.timelines.public.statuses).to.eql([status])
|
||||
expect(state.timelines.public.visibleStatuses).to.eql([])
|
||||
expect(state.timelines.public.newStatusCount).to.equal(1)
|
||||
|
||||
expect(state.allStatuses).to.eql([status])
|
||||
expect(state.timelines.friends.statuses).to.eql([status])
|
||||
expect(state.timelines.friends.visibleStatuses).to.eql([])
|
||||
expect(state.timelines.friends.newStatusCount).to.equal(1)
|
||||
})
|
||||
|
||||
it('add the statuses to allStatuses if no timeline is given', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status] })
|
||||
|
||||
expect(state.allStatuses).to.eql([status])
|
||||
expect(state.timelines.public.statuses).to.eql([])
|
||||
expect(state.timelines.public.visibleStatuses).to.eql([])
|
||||
expect(state.timelines.public.newStatusCount).to.equal(0)
|
||||
})
|
||||
|
||||
it('adds the status to allStatuses and to the given timeline, directly visible', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
|
||||
expect(state.allStatuses).to.eql([status])
|
||||
expect(state.timelines.public.statuses).to.eql([status])
|
||||
expect(state.timelines.public.visibleStatuses).to.eql([status])
|
||||
expect(state.timelines.public.newStatusCount).to.equal(0)
|
||||
})
|
||||
|
||||
it('removes statuses by tag on deletion', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
const otherStatus = makeMockStatus({id: '3'})
|
||||
status.uri = 'xxx'
|
||||
const deletion = makeMockStatus({id: '2', type: 'deletion'})
|
||||
deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
|
||||
deletion.uri = 'xxx'
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status, otherStatus], showImmediately: true, timeline: 'public' })
|
||||
mutations.addNewStatuses(state, { statuses: [deletion], showImmediately: true, timeline: 'public' })
|
||||
|
||||
expect(state.allStatuses).to.eql([otherStatus])
|
||||
expect(state.timelines.public.statuses).to.eql([otherStatus])
|
||||
expect(state.timelines.public.visibleStatuses).to.eql([otherStatus])
|
||||
expect(state.timelines.public.maxId).to.eql('3')
|
||||
})
|
||||
|
||||
it('does not update the maxId when the noIdUpdate flag is set', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
const secondStatus = makeMockStatus({id: '2'})
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
expect(state.timelines.public.maxId).to.eql('1')
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [secondStatus], showImmediately: true, timeline: 'public', noIdUpdate: true })
|
||||
expect(state.timelines.public.statuses).to.eql([secondStatus, status])
|
||||
expect(state.timelines.public.visibleStatuses).to.eql([secondStatus, status])
|
||||
expect(state.timelines.public.maxId).to.eql('1')
|
||||
})
|
||||
|
||||
it('keeps a descending by id order in timeline.visibleStatuses and timeline.statuses', () => {
|
||||
const state = defaultState()
|
||||
const nonVisibleStatus = makeMockStatus({id: '1'})
|
||||
const status = makeMockStatus({id: '3'})
|
||||
const statusTwo = makeMockStatus({id: '2'})
|
||||
const statusThree = makeMockStatus({id: '4'})
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [nonVisibleStatus], showImmediately: false, timeline: 'public' })
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
mutations.addNewStatuses(state, { statuses: [statusTwo], showImmediately: true, timeline: 'public' })
|
||||
|
||||
expect(state.timelines.public.minVisibleId).to.equal('2')
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [statusThree], showImmediately: true, timeline: 'public' })
|
||||
|
||||
expect(state.timelines.public.statuses).to.eql([statusThree, status, statusTwo, nonVisibleStatus])
|
||||
expect(state.timelines.public.visibleStatuses).to.eql([statusThree, status, statusTwo])
|
||||
})
|
||||
|
||||
it('splits retweets from their status and links them', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
const retweet = makeMockStatus({id: '2', type: 'retweet'})
|
||||
const modStatus = makeMockStatus({id: '1', text: 'something else'})
|
||||
|
||||
retweet.retweeted_status = status
|
||||
|
||||
// It adds both statuses, but only the retweet to visible.
|
||||
mutations.addNewStatuses(state, { statuses: [retweet], timeline: 'public', showImmediately: true })
|
||||
expect(state.timelines.public.visibleStatuses).to.have.length(1)
|
||||
expect(state.timelines.public.statuses).to.have.length(1)
|
||||
expect(state.allStatuses).to.have.length(2)
|
||||
expect(state.allStatuses[0].id).to.equal('1')
|
||||
expect(state.allStatuses[1].id).to.equal('2')
|
||||
|
||||
// It refers to the modified status.
|
||||
mutations.addNewStatuses(state, { statuses: [modStatus], timeline: 'public' })
|
||||
expect(state.allStatuses).to.have.length(2)
|
||||
expect(state.allStatuses[0].id).to.equal('1')
|
||||
expect(state.allStatuses[0].text).to.equal(modStatus.text)
|
||||
expect(state.allStatuses[1].id).to.equal('2')
|
||||
expect(retweet.retweeted_status.text).to.eql(modStatus.text)
|
||||
})
|
||||
|
||||
it('replaces existing statuses with the same id', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
const modStatus = makeMockStatus({id: '1', text: 'something else'})
|
||||
|
||||
// Add original status
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
expect(state.timelines.public.visibleStatuses).to.have.length(1)
|
||||
expect(state.allStatuses).to.have.length(1)
|
||||
|
||||
// Add new version of status
|
||||
mutations.addNewStatuses(state, { statuses: [modStatus], showImmediately: true, timeline: 'public' })
|
||||
expect(state.timelines.public.visibleStatuses).to.have.length(1)
|
||||
expect(state.allStatuses).to.have.length(1)
|
||||
expect(state.allStatuses[0].text).to.eql(modStatus.text)
|
||||
})
|
||||
|
||||
it('replaces existing statuses with the same id, coming from a retweet', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
const modStatus = makeMockStatus({id: '1', text: 'something else'})
|
||||
const retweet = makeMockStatus({id: '2', type: 'retweet'})
|
||||
retweet.retweeted_status = modStatus
|
||||
|
||||
// Add original status
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
expect(state.timelines.public.visibleStatuses).to.have.length(1)
|
||||
expect(state.allStatuses).to.have.length(1)
|
||||
|
||||
// Add new version of status
|
||||
mutations.addNewStatuses(state, { statuses: [retweet], showImmediately: false, timeline: 'public' })
|
||||
expect(state.timelines.public.visibleStatuses).to.have.length(1)
|
||||
// Don't add the retweet itself if the tweet is visible
|
||||
expect(state.timelines.public.statuses).to.have.length(1)
|
||||
expect(state.allStatuses).to.have.length(2)
|
||||
expect(state.allStatuses[0].text).to.eql(modStatus.text)
|
||||
})
|
||||
|
||||
it('handles favorite actions', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
|
||||
const favorite = {
|
||||
id: '2',
|
||||
type: 'favorite',
|
||||
in_reply_to_status_id: '1', // The API uses strings here...
|
||||
uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
|
||||
text: 'a favorited something by b',
|
||||
user: { id: '99' }
|
||||
}
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
mutations.addNewStatuses(state, { statuses: [favorite], showImmediately: true, timeline: 'public' })
|
||||
|
||||
expect(state.timelines.public.visibleStatuses.length).to.eql(1)
|
||||
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
|
||||
expect(state.timelines.public.maxId).to.eq(favorite.id)
|
||||
|
||||
// Adding it again does nothing
|
||||
mutations.addNewStatuses(state, { statuses: [favorite], showImmediately: true, timeline: 'public' })
|
||||
|
||||
expect(state.timelines.public.visibleStatuses.length).to.eql(1)
|
||||
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
|
||||
expect(state.timelines.public.maxId).to.eq(favorite.id)
|
||||
|
||||
// If something is favorited by the current user, it also sets the 'favorited' property but does not increment counter to avoid over-counting. Counter is incremented (updated, really) via response to the favorite request.
|
||||
const user = {
|
||||
id: '1'
|
||||
}
|
||||
|
||||
const ownFavorite = {
|
||||
id: '3',
|
||||
type: 'favorite',
|
||||
in_reply_to_status_id: '1', // The API uses strings here...
|
||||
uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
|
||||
text: 'a favorited something by b',
|
||||
user
|
||||
}
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [ownFavorite], showImmediately: true, timeline: 'public', user })
|
||||
|
||||
expect(state.timelines.public.visibleStatuses.length).to.eql(1)
|
||||
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
|
||||
expect(state.timelines.public.visibleStatuses[0].favorited).to.eql(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('add the statuses to allStatuses if no timeline is given', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
describe('showNewStatuses', () => {
|
||||
it('resets the minId to the min of the visible statuses when adding new to visible statuses', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({ id: '10' })
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
const newStatus = makeMockStatus({ id: '20' })
|
||||
mutations.addNewStatuses(state, { statuses: [newStatus], showImmediately: false, timeline: 'public' })
|
||||
state.timelines.public.minId = '5'
|
||||
mutations.showNewStatuses(state, { timeline: 'public' })
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status] })
|
||||
|
||||
expect(state.allStatuses).to.eql([status])
|
||||
expect(state.timelines.public.statuses).to.eql([])
|
||||
expect(state.timelines.public.visibleStatuses).to.eql([])
|
||||
expect(state.timelines.public.newStatusCount).to.equal(0)
|
||||
expect(state.timelines.public.visibleStatuses.length).to.eql(2)
|
||||
expect(state.timelines.public.minVisibleId).to.eql('10')
|
||||
expect(state.timelines.public.minId).to.eql('10')
|
||||
})
|
||||
})
|
||||
|
||||
it('adds the status to allStatuses and to the given timeline, directly visible', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
describe('clearTimeline', () => {
|
||||
it('keeps userId when clearing user timeline', () => {
|
||||
const state = defaultState()
|
||||
state.timelines.user.userId = 123
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
mutations.clearTimeline(state, { timeline: 'user' })
|
||||
|
||||
expect(state.allStatuses).to.eql([status])
|
||||
expect(state.timelines.public.statuses).to.eql([status])
|
||||
expect(state.timelines.public.visibleStatuses).to.eql([status])
|
||||
expect(state.timelines.public.newStatusCount).to.equal(0)
|
||||
})
|
||||
|
||||
it('removes statuses by tag on deletion', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
const otherStatus = makeMockStatus({id: '3'})
|
||||
status.uri = 'xxx'
|
||||
const deletion = makeMockStatus({id: '2', type: 'deletion'})
|
||||
deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
|
||||
deletion.uri = 'xxx'
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status, otherStatus], showImmediately: true, timeline: 'public' })
|
||||
mutations.addNewStatuses(state, { statuses: [deletion], showImmediately: true, timeline: 'public' })
|
||||
|
||||
expect(state.allStatuses).to.eql([otherStatus])
|
||||
expect(state.timelines.public.statuses).to.eql([otherStatus])
|
||||
expect(state.timelines.public.visibleStatuses).to.eql([otherStatus])
|
||||
expect(state.timelines.public.maxId).to.eql('3')
|
||||
})
|
||||
|
||||
it('does not update the maxId when the noIdUpdate flag is set', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
const secondStatus = makeMockStatus({id: '2'})
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
expect(state.timelines.public.maxId).to.eql('1')
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [secondStatus], showImmediately: true, timeline: 'public', noIdUpdate: true })
|
||||
expect(state.timelines.public.statuses).to.eql([secondStatus, status])
|
||||
expect(state.timelines.public.visibleStatuses).to.eql([secondStatus, status])
|
||||
expect(state.timelines.public.maxId).to.eql('1')
|
||||
})
|
||||
|
||||
it('keeps a descending by id order in timeline.visibleStatuses and timeline.statuses', () => {
|
||||
const state = defaultState()
|
||||
const nonVisibleStatus = makeMockStatus({id: '1'})
|
||||
const status = makeMockStatus({id: '3'})
|
||||
const statusTwo = makeMockStatus({id: '2'})
|
||||
const statusThree = makeMockStatus({id: '4'})
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [nonVisibleStatus], showImmediately: false, timeline: 'public' })
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
mutations.addNewStatuses(state, { statuses: [statusTwo], showImmediately: true, timeline: 'public' })
|
||||
|
||||
expect(state.timelines.public.minVisibleId).to.equal('2')
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [statusThree], showImmediately: true, timeline: 'public' })
|
||||
|
||||
expect(state.timelines.public.statuses).to.eql([statusThree, status, statusTwo, nonVisibleStatus])
|
||||
expect(state.timelines.public.visibleStatuses).to.eql([statusThree, status, statusTwo])
|
||||
})
|
||||
|
||||
it('splits retweets from their status and links them', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
const retweet = makeMockStatus({id: '2', type: 'retweet'})
|
||||
const modStatus = makeMockStatus({id: '1', text: 'something else'})
|
||||
|
||||
retweet.retweeted_status = status
|
||||
|
||||
// It adds both statuses, but only the retweet to visible.
|
||||
mutations.addNewStatuses(state, { statuses: [retweet], timeline: 'public', showImmediately: true })
|
||||
expect(state.timelines.public.visibleStatuses).to.have.length(1)
|
||||
expect(state.timelines.public.statuses).to.have.length(1)
|
||||
expect(state.allStatuses).to.have.length(2)
|
||||
expect(state.allStatuses[0].id).to.equal('1')
|
||||
expect(state.allStatuses[1].id).to.equal('2')
|
||||
|
||||
// It refers to the modified status.
|
||||
mutations.addNewStatuses(state, { statuses: [modStatus], timeline: 'public' })
|
||||
expect(state.allStatuses).to.have.length(2)
|
||||
expect(state.allStatuses[0].id).to.equal('1')
|
||||
expect(state.allStatuses[0].text).to.equal(modStatus.text)
|
||||
expect(state.allStatuses[1].id).to.equal('2')
|
||||
expect(retweet.retweeted_status.text).to.eql(modStatus.text)
|
||||
})
|
||||
|
||||
it('replaces existing statuses with the same id', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
const modStatus = makeMockStatus({id: '1', text: 'something else'})
|
||||
|
||||
// Add original status
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
expect(state.timelines.public.visibleStatuses).to.have.length(1)
|
||||
expect(state.allStatuses).to.have.length(1)
|
||||
|
||||
// Add new version of status
|
||||
mutations.addNewStatuses(state, { statuses: [modStatus], showImmediately: true, timeline: 'public' })
|
||||
expect(state.timelines.public.visibleStatuses).to.have.length(1)
|
||||
expect(state.allStatuses).to.have.length(1)
|
||||
expect(state.allStatuses[0].text).to.eql(modStatus.text)
|
||||
})
|
||||
|
||||
it('replaces existing statuses with the same id, coming from a retweet', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
const modStatus = makeMockStatus({id: '1', text: 'something else'})
|
||||
const retweet = makeMockStatus({id: '2', type: 'retweet'})
|
||||
retweet.retweeted_status = modStatus
|
||||
|
||||
// Add original status
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
expect(state.timelines.public.visibleStatuses).to.have.length(1)
|
||||
expect(state.allStatuses).to.have.length(1)
|
||||
|
||||
// Add new version of status
|
||||
mutations.addNewStatuses(state, { statuses: [retweet], showImmediately: false, timeline: 'public' })
|
||||
expect(state.timelines.public.visibleStatuses).to.have.length(1)
|
||||
// Don't add the retweet itself if the tweet is visible
|
||||
expect(state.timelines.public.statuses).to.have.length(1)
|
||||
expect(state.allStatuses).to.have.length(2)
|
||||
expect(state.allStatuses[0].text).to.eql(modStatus.text)
|
||||
})
|
||||
|
||||
it('handles favorite actions', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({id: '1'})
|
||||
|
||||
const favorite = {
|
||||
id: '2',
|
||||
type: 'favorite',
|
||||
in_reply_to_status_id: '1', // The API uses strings here...
|
||||
uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
|
||||
text: 'a favorited something by b',
|
||||
user: { id: '99' }
|
||||
}
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
mutations.addNewStatuses(state, { statuses: [favorite], showImmediately: true, timeline: 'public' })
|
||||
|
||||
expect(state.timelines.public.visibleStatuses.length).to.eql(1)
|
||||
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
|
||||
expect(state.timelines.public.maxId).to.eq(favorite.id)
|
||||
|
||||
// Adding it again does nothing
|
||||
mutations.addNewStatuses(state, { statuses: [favorite], showImmediately: true, timeline: 'public' })
|
||||
|
||||
expect(state.timelines.public.visibleStatuses.length).to.eql(1)
|
||||
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
|
||||
expect(state.timelines.public.maxId).to.eq(favorite.id)
|
||||
|
||||
// If something is favorited by the current user, it also sets the 'favorited' property but does not increment counter to avoid over-counting. Counter is incremented (updated, really) via response to the favorite request.
|
||||
const user = {
|
||||
id: '1'
|
||||
}
|
||||
|
||||
const ownFavorite = {
|
||||
id: '3',
|
||||
type: 'favorite',
|
||||
in_reply_to_status_id: '1', // The API uses strings here...
|
||||
uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
|
||||
text: 'a favorited something by b',
|
||||
user
|
||||
}
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [ownFavorite], showImmediately: true, timeline: 'public', user })
|
||||
|
||||
expect(state.timelines.public.visibleStatuses.length).to.eql(1)
|
||||
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
|
||||
expect(state.timelines.public.visibleStatuses[0].favorited).to.eql(true)
|
||||
})
|
||||
|
||||
it('keeps userId when clearing user timeline', () => {
|
||||
const state = defaultState()
|
||||
state.timelines.user.userId = 123
|
||||
|
||||
mutations.clearTimeline(state, { timeline: 'user' })
|
||||
|
||||
expect(state.timelines.user.userId).to.eql(123)
|
||||
expect(state.timelines.user.userId).to.eql(123)
|
||||
})
|
||||
})
|
||||
|
||||
describe('notifications', () => {
|
||||
|
|
|
@ -34,40 +34,31 @@ describe('The users module', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('getUserByName', () => {
|
||||
describe('findUser', () => {
|
||||
it('returns user with matching screen_name', () => {
|
||||
const user = { screen_name: 'Guy', id: '1' }
|
||||
const state = {
|
||||
users: [
|
||||
{ screen_name: 'Guy', id: '1' }
|
||||
]
|
||||
usersObject: {
|
||||
1: user,
|
||||
guy: user
|
||||
}
|
||||
}
|
||||
const name = 'Guy'
|
||||
const expected = { screen_name: 'Guy', id: '1' }
|
||||
expect(getters.userByName(state)(name)).to.eql(expected)
|
||||
expect(getters.findUser(state)(name)).to.eql(expected)
|
||||
})
|
||||
|
||||
it('returns user with matching screen_name with different case', () => {
|
||||
const state = {
|
||||
users: [
|
||||
{ screen_name: 'guy', id: '1' }
|
||||
]
|
||||
}
|
||||
const name = 'Guy'
|
||||
const expected = { screen_name: 'guy', id: '1' }
|
||||
expect(getters.userByName(state)(name)).to.eql(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getUserById', () => {
|
||||
it('returns user with matching id', () => {
|
||||
const user = { screen_name: 'Guy', id: '1' }
|
||||
const state = {
|
||||
users: [
|
||||
{ screen_name: 'Guy', id: '1' }
|
||||
]
|
||||
usersObject: {
|
||||
1: user,
|
||||
guy: user
|
||||
}
|
||||
}
|
||||
const id = '1'
|
||||
const expected = { screen_name: 'Guy', id: '1' }
|
||||
expect(getters.userById(state)(id)).to.eql(expected)
|
||||
expect(getters.findUser(state)(id)).to.eql(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { parseStatus, parseUser, parseNotification } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
|
||||
import { parseStatus, parseUser, parseNotification, addEmojis } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
|
||||
import mastoapidata from '../../../../fixtures/mastoapi.json'
|
||||
import qvitterapidata from '../../../../fixtures/statuses.json'
|
||||
|
||||
|
@ -143,6 +143,23 @@ const makeMockNotificationQvitter = (overrides = {}) => {
|
|||
}, overrides)
|
||||
}
|
||||
|
||||
const makeMockEmojiMasto = (overrides = [{}]) => {
|
||||
return [
|
||||
Object.assign({
|
||||
shortcode: 'image',
|
||||
static_url: 'https://example.com/image.png',
|
||||
url: 'https://example.com/image.png',
|
||||
visible_in_picker: false
|
||||
}, overrides[0]),
|
||||
Object.assign({
|
||||
shortcode: 'thinking',
|
||||
static_url: 'https://example.com/think.png',
|
||||
url: 'https://example.com/think.png',
|
||||
visible_in_picker: false
|
||||
}, overrides[1])
|
||||
]
|
||||
}
|
||||
|
||||
parseNotification
|
||||
parseUser
|
||||
parseStatus
|
||||
|
@ -218,6 +235,22 @@ describe('API Entities normalizer', () => {
|
|||
expect(parsedRepeat).to.have.property('retweeted_status')
|
||||
expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef')
|
||||
})
|
||||
|
||||
it('adds emojis to post content', () => {
|
||||
const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), content: 'Makes you think :thinking:' })
|
||||
|
||||
const parsedPost = parseStatus(post)
|
||||
|
||||
expect(parsedPost).to.have.property('statusnet_html').that.contains('<img')
|
||||
})
|
||||
|
||||
it('adds emojis to subject line', () => {
|
||||
const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), spoiler_text: 'CW: 300 IQ :thinking:' })
|
||||
|
||||
const parsedPost = parseStatus(post)
|
||||
|
||||
expect(parsedPost).to.have.property('summary_html').that.contains('<img')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -230,6 +263,22 @@ describe('API Entities normalizer', () => {
|
|||
expect(parseUser(local)).to.have.property('is_local', true)
|
||||
expect(parseUser(remote)).to.have.property('is_local', false)
|
||||
})
|
||||
|
||||
it('adds emojis to user name', () => {
|
||||
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), display_name: 'The :thinking: thinker' })
|
||||
|
||||
const parsedUser = parseUser(user)
|
||||
|
||||
expect(parsedUser).to.have.property('name_html').that.contains('<img')
|
||||
})
|
||||
|
||||
it('adds emojis to user bio', () => {
|
||||
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), note: 'Hello i like to :thinking: a lot' })
|
||||
|
||||
const parsedUser = parseUser(user)
|
||||
|
||||
expect(parsedUser).to.have.property('description_html').that.contains('<img')
|
||||
})
|
||||
})
|
||||
|
||||
// We currently use QvitterAPI notifications only, and especially due to MastoAPI lacking is_seen, support for MastoAPI
|
||||
|
@ -267,4 +316,28 @@ describe('API Entities normalizer', () => {
|
|||
expect(parseNotification(notif)).to.have.deep.property('from_profile.id', 'spurdo')
|
||||
})
|
||||
})
|
||||
|
||||
describe('MastoAPI emoji adder', () => {
|
||||
const emojis = makeMockEmojiMasto()
|
||||
const imageHtml = '<img src="https://example.com/image.png" alt="image" class="emoji" />'
|
||||
.replace(/"/g, '\'')
|
||||
const thinkHtml = '<img src="https://example.com/think.png" alt="thinking" class="emoji" />'
|
||||
.replace(/"/g, '\'')
|
||||
|
||||
it('correctly replaces shortcodes in supplied string', () => {
|
||||
const result = addEmojis('This post has :image: emoji and :thinking: emoji', emojis)
|
||||
expect(result).to.include(thinkHtml)
|
||||
expect(result).to.include(imageHtml)
|
||||
})
|
||||
|
||||
it('handles consecutive emojis correctly', () => {
|
||||
const result = addEmojis('Lelel emoji spam :thinking::thinking::thinking::thinking:', emojis)
|
||||
expect(result).to.include(thinkHtml + thinkHtml + thinkHtml + thinkHtml)
|
||||
})
|
||||
|
||||
it('Doesn\'t replace nonexistent emojis', () => {
|
||||
const result = addEmojis('Admin add the :tenshi: emoji', emojis)
|
||||
expect(result).to.equal('Admin add the :tenshi: emoji')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue