Merge remote-tracking branch 'upstream/develop' into shigusegubu
* upstream/develop: (49 commits) linting update test names confusion better handling of attachments Linting. Don't use referrerpolicy with media proxy. update logo support for extended fields (for future, doesn't work yet), fix reply bug more fields for users some more post fields support for CW/Subject. fix replies. removing unnecessary conversions since it should already be converted in normalizer fix indents some consistency localization strings add support for tab-switcher to automatically switch to first tab if asked index is invalid fix login and favorites tab... Revert "some initial work to make it possible to use "unregistered" timelines, i.e. not" and some stuff to make favorites still work forgot the file tests for the tests god! bugfixes for bugfixes throne! ...
This commit is contained in:
commit
657bcf72fb
40 changed files with 4417 additions and 889 deletions
|
@ -16,6 +16,8 @@ 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'
|
||||
import About from 'components/about/about.vue'
|
||||
|
||||
export default (store) => {
|
||||
return [
|
||||
|
@ -46,6 +48,8 @@ export default (store) => {
|
|||
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||
{ name: 'user-search', path: '/user-search', component: UserSearch, props: (route) => ({ query: route.query.query }) },
|
||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow },
|
||||
{ name: 'about', path: '/about', component: About },
|
||||
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
|
||||
]
|
||||
}
|
||||
|
|
13
src/components/about/about.js
Normal file
13
src/components/about/about.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
|
||||
import FeaturesPanel from '../features_panel/features_panel.vue'
|
||||
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
|
||||
|
||||
const About = {
|
||||
components: {
|
||||
InstanceSpecificPanel,
|
||||
FeaturesPanel,
|
||||
TermsOfServicePanel
|
||||
}
|
||||
}
|
||||
|
||||
export default About
|
12
src/components/about/about.vue
Normal file
12
src/components/about/about.vue
Normal file
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<div class="sidebar">
|
||||
<instance-specific-panel></instance-specific-panel>
|
||||
<features-panel></features-panel>
|
||||
<terms-of-service-panel></terms-of-service-panel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./about.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
|
@ -24,6 +24,9 @@ const Attachment = {
|
|||
StillImage
|
||||
},
|
||||
computed: {
|
||||
referrerpolicy () {
|
||||
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
||||
},
|
||||
type () {
|
||||
return fileTypeService.fileType(this.attachment.mimetype)
|
||||
},
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<a href="#" @click.prevent="toggleHidden()">Hide</a>
|
||||
</div>
|
||||
<a v-if="type === 'image' && (!hidden || preloadImage)" class="image-attachment" :class="{'hidden': hidden && preloadImage}" :href="attachment.url" target="_blank" :title="attachment.description">
|
||||
<StillImage :class="{'small': isSmall}" referrerpolicy="no-referrer" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
|
||||
<StillImage :class="{'small': isSmall}" :referrerpolicy="referrerPolicy" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
|
||||
</a>
|
||||
|
||||
<video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" @loadeddata="onVideoDataLoad" :src="attachment.url" controls :loop="loopVideo" playsinline></video>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import Conversation from '../conversation/conversation.vue'
|
||||
import { find, toInteger } from 'lodash'
|
||||
import { find } from 'lodash'
|
||||
|
||||
const conversationPage = {
|
||||
components: {
|
||||
|
@ -7,7 +7,7 @@ const conversationPage = {
|
|||
},
|
||||
computed: {
|
||||
statusoid () {
|
||||
const id = toInteger(this.$route.params.id)
|
||||
const id = this.$route.params.id
|
||||
const statuses = this.$store.state.statuses.allStatuses
|
||||
const status = find(statuses, {id})
|
||||
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { reduce, filter, sortBy } from 'lodash'
|
||||
import { statusType } from '../../modules/statuses.js'
|
||||
import Status from '../status/status.vue'
|
||||
|
||||
const sortAndFilterConversation = (conversation) => {
|
||||
conversation = filter(conversation, (status) => statusType(status) !== 'retweet')
|
||||
conversation = filter(conversation, (status) => status.type !== 'retweet')
|
||||
return sortBy(conversation, 'id')
|
||||
}
|
||||
|
||||
|
@ -18,10 +17,12 @@ const conversation = {
|
|||
'collapsable'
|
||||
],
|
||||
computed: {
|
||||
status () { return this.statusoid },
|
||||
status () {
|
||||
return this.statusoid
|
||||
},
|
||||
conversation () {
|
||||
if (!this.status) {
|
||||
return false
|
||||
return []
|
||||
}
|
||||
|
||||
const conversationId = this.status.statusnet_conversation_id
|
||||
|
@ -32,7 +33,9 @@ const conversation = {
|
|||
replies () {
|
||||
let i = 1
|
||||
return reduce(this.conversation, (result, {id, in_reply_to_status_id}) => {
|
||||
const irid = Number(in_reply_to_status_id)
|
||||
/* eslint-disable camelcase */
|
||||
const irid = in_reply_to_status_id
|
||||
/* eslint-enable camelcase */
|
||||
if (irid) {
|
||||
result[irid] = result[irid] || []
|
||||
result[irid].push({
|
||||
|
@ -69,7 +72,6 @@ const conversation = {
|
|||
}
|
||||
},
|
||||
getReplies (id) {
|
||||
id = Number(id)
|
||||
return this.replies[id] || []
|
||||
},
|
||||
focused (id) {
|
||||
|
@ -80,7 +82,7 @@ const conversation = {
|
|||
}
|
||||
},
|
||||
setHighlight (id) {
|
||||
this.highlight = Number(id)
|
||||
this.highlight = id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,9 @@ const SideDrawer = {
|
|||
},
|
||||
unseenNotificationsCount () {
|
||||
return this.unseenNotifications.length
|
||||
},
|
||||
suggestionsEnabled () {
|
||||
return this.$store.state.instance.suggestionsEnabled
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -62,15 +62,25 @@
|
|||
</ul>
|
||||
<ul>
|
||||
<li @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'user-search'}">
|
||||
<router-link :to="{ name: 'user-search' }">
|
||||
{{ $t("nav.user_search") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser && suggestionsEnabled" @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'who-to-follow' }">
|
||||
{{ $t("nav.who_to_follow") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'settings'}">
|
||||
<router-link :to="{ name: 'settings' }">
|
||||
{{ $t("settings.settings") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'about'}">
|
||||
{{ $t("nav.about") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="currentUser" @click="toggleDrawer">
|
||||
<a @click="doLogout" href="#">
|
||||
{{ $t("login.logout") }}
|
||||
|
|
|
@ -117,19 +117,7 @@ const Status = {
|
|||
return lengthScore > 20
|
||||
},
|
||||
isReply () {
|
||||
if (this.status.in_reply_to_status_id) {
|
||||
return true
|
||||
}
|
||||
// For private replies where we can't see the OP, in_reply_to_status_id will be null.
|
||||
// So instead, check that the post starts with a @mention.
|
||||
if (this.status.visibility === 'private') {
|
||||
var textBody = this.status.text
|
||||
if (this.status.summary !== null) {
|
||||
textBody = textBody.substring(this.status.summary.length, textBody.length)
|
||||
}
|
||||
return textBody.startsWith('@')
|
||||
}
|
||||
return false
|
||||
return !!this.status.in_reply_to_status_id
|
||||
},
|
||||
hideReply () {
|
||||
if (this.$store.state.config.replyVisibility === 'all') {
|
||||
|
@ -141,7 +129,7 @@ const Status = {
|
|||
if (this.status.user.id === this.$store.state.users.currentUser.id) {
|
||||
return false
|
||||
}
|
||||
if (this.status.activity_type === 'repeat') {
|
||||
if (this.status.type === 'retweet') {
|
||||
return false
|
||||
}
|
||||
var checkFollowing = this.$store.state.config.replyVisibility === 'following'
|
||||
|
@ -270,7 +258,7 @@ const Status = {
|
|||
},
|
||||
replyEnter (id, event) {
|
||||
this.showPreview = true
|
||||
const targetId = Number(id)
|
||||
const targetId = id
|
||||
const statuses = this.$store.state.statuses.allStatuses
|
||||
|
||||
if (!this.preview) {
|
||||
|
@ -295,7 +283,6 @@ const Status = {
|
|||
},
|
||||
watch: {
|
||||
'highlight': function (id) {
|
||||
id = Number(id)
|
||||
if (this.status.id === id) {
|
||||
let rect = this.$el.getBoundingClientRect()
|
||||
if (rect.top < 100) {
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
<div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper">
|
||||
<a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowMore">Show more</a>
|
||||
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html" v-if="!hideSubjectStatus"></div>
|
||||
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary" v-else></div>
|
||||
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary_html" v-else></div>
|
||||
<a v-if="hideSubjectStatus" href="#" class="cw-status-hider" @click.prevent="toggleShowMore">Show more</a>
|
||||
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">Show less</a>
|
||||
</div>
|
||||
|
|
|
@ -6,18 +6,26 @@ export default Vue.component('tab-switcher', {
|
|||
name: 'TabSwitcher',
|
||||
data () {
|
||||
return {
|
||||
active: 0
|
||||
active: this.$slots.default.findIndex(_ => _.tag)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
activateTab(index) {
|
||||
return () => this.active = index;
|
||||
activateTab (index) {
|
||||
return () => {
|
||||
this.active = index
|
||||
}
|
||||
}
|
||||
},
|
||||
render(h) {
|
||||
beforeUpdate () {
|
||||
const currentSlot = this.$slots.default[this.active]
|
||||
if (!currentSlot.tag) {
|
||||
this.active = this.$slots.default.findIndex(_ => _.tag)
|
||||
}
|
||||
},
|
||||
render (h) {
|
||||
const tabs = this.$slots.default
|
||||
.filter(slot => slot.data)
|
||||
.map((slot, index) => {
|
||||
if (!slot.tag) return
|
||||
const classesTab = ['tab']
|
||||
const classesWrapper = ['tab-wrapper']
|
||||
|
||||
|
@ -25,20 +33,24 @@ export default Vue.component('tab-switcher', {
|
|||
classesTab.push('active')
|
||||
classesWrapper.push('active')
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={ classesWrapper.join(' ')}>
|
||||
<button onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
const contents = this.$slots.default.filter(_=>_.data).map(( slot, index ) => {
|
||||
})
|
||||
|
||||
const contents = this.$slots.default.map((slot, index) => {
|
||||
if (!slot.tag) return
|
||||
const active = index === this.active
|
||||
return (
|
||||
<div class={active ? 'active' : 'hidden'}>
|
||||
{slot}
|
||||
</div>
|
||||
)
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="tab-switcher">
|
||||
<div class="tabs">
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
const TermsOfServicePanel = {
|
||||
computed: {
|
||||
content () {
|
||||
return this.$store.state.instance.tos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default TermsOfServicePanel
|
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div v-html="content" class="tos-content">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./terms_of_service_panel.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
.tos-content {
|
||||
margin: 1em
|
||||
}
|
||||
</style>
|
|
@ -142,7 +142,7 @@
|
|||
border-bottom-right-radius: 0;
|
||||
|
||||
.panel-heading {
|
||||
padding: .6em 0;
|
||||
padding: .5em 0;
|
||||
text-align: center;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
@ -226,10 +226,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.user-name{
|
||||
.user-name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
flex: 1 0 auto;
|
||||
flex: 1 1 auto;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.user-screen-name {
|
||||
|
@ -245,6 +246,10 @@
|
|||
.dailyAvg {
|
||||
min-width: 1px;
|
||||
flex: 0 0 auto;
|
||||
margin-left: 1em;
|
||||
font-size: 0.7em;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
|
||||
.handle {
|
||||
|
@ -381,11 +386,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.dailyAvg {
|
||||
margin-left: 1em;
|
||||
font-size: 0.7em;
|
||||
color: #CCC;
|
||||
}
|
||||
.floater {
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,23 +5,32 @@ import Timeline from '../timeline/timeline.vue'
|
|||
const UserProfile = {
|
||||
created () {
|
||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
||||
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
|
||||
if (!this.user.id) {
|
||||
this.$store.dispatch('fetchUser', this.fetchBy)
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
this.$store.dispatch('stopFetching', 'user')
|
||||
this.$store.dispatch('stopFetching', 'favorites')
|
||||
},
|
||||
computed: {
|
||||
timeline () {
|
||||
return this.$store.state.statuses.timelines.user
|
||||
},
|
||||
favorites () {
|
||||
return this.$store.state.statuses.timelines.favorites
|
||||
},
|
||||
userId () {
|
||||
return this.$route.params.id || this.user.id
|
||||
},
|
||||
userName () {
|
||||
return this.$route.params.name
|
||||
return this.$route.params.name || this.user.screen_name
|
||||
},
|
||||
isUs () {
|
||||
return this.userId === this.$store.state.users.currentUser.id
|
||||
},
|
||||
friends () {
|
||||
return this.user.friends
|
||||
|
@ -62,21 +71,28 @@ const UserProfile = {
|
|||
}
|
||||
},
|
||||
watch: {
|
||||
// TODO get rid of this copypasta
|
||||
userName () {
|
||||
if (this.isExternal) {
|
||||
return
|
||||
}
|
||||
this.$store.dispatch('stopFetching', 'user')
|
||||
this.$store.dispatch('stopFetching', 'favorites')
|
||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||
this.$store.dispatch('startFetching', ['user', this.userName])
|
||||
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
||||
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
|
||||
},
|
||||
userId () {
|
||||
if (!this.isExternal) {
|
||||
return
|
||||
}
|
||||
this.$store.dispatch('stopFetching', 'user')
|
||||
this.$store.dispatch('stopFetching', 'favorites')
|
||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||
this.$store.dispatch('startFetching', ['user', this.userId])
|
||||
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
||||
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
|
||||
},
|
||||
user () {
|
||||
if (this.user.id && !this.user.followers) {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div v-if="user.id" class="user-profile panel panel-default">
|
||||
<user-card-content :user="user" :switcher="true" :selected="timeline.viewing"></user-card-content>
|
||||
<tab-switcher>
|
||||
<Timeline :label="$t('user_card.statuses')" :embedded="true" :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="userId"/>
|
||||
<Timeline :label="$t('user_card.statuses')" :embedded="true" :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="fetchBy"/>
|
||||
<div :label="$t('user_card.followees')">
|
||||
<div v-if="friends">
|
||||
<user-card v-for="friend in friends" :key="friend.id" :user="friend" :showFollows="true"></user-card>
|
||||
|
@ -20,6 +20,7 @@
|
|||
<i class="icon-spin3 animate-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<Timeline v-if="isUs" :label="$t('user_card.favorites')" :embedded="true" :title="$t('user_profile.favorites_title')" timeline-name="favorites" :timeline="favorites"/>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
<div v-else class="panel user-profile-placeholder">
|
||||
|
|
48
src/components/who_to_follow/who_to_follow.js
Normal file
48
src/components/who_to_follow/who_to_follow.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
import apiService from '../../services/api/api.service.js'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
|
||||
const WhoToFollow = {
|
||||
components: {
|
||||
UserCard
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
users: []
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getWhoToFollow()
|
||||
},
|
||||
methods: {
|
||||
showWhoToFollow (reply) {
|
||||
reply.forEach((i, index) => {
|
||||
const user = {
|
||||
id: 0,
|
||||
name: i.display_name,
|
||||
screen_name: i.acct,
|
||||
profile_image_url: i.avatar || '/images/avi.png'
|
||||
}
|
||||
this.users.push(user)
|
||||
|
||||
this.$store.state.api.backendInteractor.externalProfile(user.screen_name)
|
||||
.then((externalUser) => {
|
||||
if (!externalUser.error) {
|
||||
this.$store.commit('addNewUsers', [externalUser])
|
||||
user.id = externalUser.id
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
getWhoToFollow () {
|
||||
const credentials = this.$store.state.users.currentUser.credentials
|
||||
if (credentials) {
|
||||
apiService.suggestions({credentials: credentials})
|
||||
.then((reply) => {
|
||||
this.showWhoToFollow(reply)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default WhoToFollow
|
15
src/components/who_to_follow/who_to_follow.vue
Normal file
15
src/components/who_to_follow/who_to_follow.vue
Normal file
|
@ -0,0 +1,15 @@
|
|||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{{$t('who_to_follow.who_to_follow')}}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./who_to_follow.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
|
@ -50,14 +50,6 @@ const WhoToFollowPanel = {
|
|||
user: function () {
|
||||
return this.$store.state.users.currentUser.screen_name
|
||||
},
|
||||
moreUrl: function () {
|
||||
const host = window.location.hostname
|
||||
const user = this.user
|
||||
const suggestionsWeb = this.$store.state.instance.suggestionsWeb
|
||||
const url = suggestionsWeb.replace(/{{host}}/g, encodeURIComponent(host))
|
||||
.replace(/{{user}}/g, encodeURIComponent(user))
|
||||
return url
|
||||
},
|
||||
suggestionsEnabled () {
|
||||
return this.$store.state.instance.suggestionsEnabled
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
{{user.name}}
|
||||
</router-link><br />
|
||||
</span>
|
||||
<img v-bind:src="$store.state.instance.logo"> <a v-bind:href="moreUrl" target="_blank">{{$t('who_to_follow.more')}}</a>
|
||||
<img v-bind:src="$store.state.instance.logo"> <router-link :to="{ name: 'who-to-follow' }">{{$t('who_to_follow.more')}}</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"username": "Username"
|
||||
},
|
||||
"nav": {
|
||||
"about": "About",
|
||||
"back": "Back",
|
||||
"chat": "Local Chat",
|
||||
"friend_requests": "Follow Requests",
|
||||
|
@ -38,6 +39,7 @@
|
|||
"timeline": "Timeline",
|
||||
"twkn": "The Whole Known Network",
|
||||
"user_search": "User Search",
|
||||
"who_to_follow": "Who to follow",
|
||||
"preferences": "Preferences"
|
||||
},
|
||||
"notifications": {
|
||||
|
@ -322,6 +324,7 @@
|
|||
"block": "Block",
|
||||
"blocked": "Blocked!",
|
||||
"deny": "Deny",
|
||||
"favorites": "Favorites",
|
||||
"follow": "Follow",
|
||||
"follow_sent": "Request sent!",
|
||||
"follow_progress": "Requesting…",
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"username": "ユーザーめい"
|
||||
},
|
||||
"nav": {
|
||||
"about": "これはなに?",
|
||||
"back": "もどる",
|
||||
"chat": "ローカルチャット",
|
||||
"friend_requests": "フォローリクエスト",
|
||||
|
@ -38,6 +39,7 @@
|
|||
"timeline": "タイムライン",
|
||||
"twkn": "つながっているすべてのネットワーク",
|
||||
"user_search": "ユーザーをさがす",
|
||||
"who_to_follow": "おすすめユーザー",
|
||||
"preferences": "せってい"
|
||||
},
|
||||
"notifications": {
|
||||
|
@ -50,6 +52,7 @@
|
|||
"repeated_you": "あなたのステータスがリピートされました"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "とうこうする",
|
||||
"account_not_locked_warning": "あなたのアカウントは {0} ではありません。あなたをフォローすれば、だれでも、フォロワーげんていのステータスをよむことができます。",
|
||||
"account_not_locked_warning_link": "ロックされたアカウント",
|
||||
"attachments_sensitive": "ファイルをNSFWにする",
|
||||
|
|
370
src/i18n/ko.json
Normal file
370
src/i18n/ko.json
Normal file
|
@ -0,0 +1,370 @@
|
|||
{
|
||||
"chat": {
|
||||
"title": "챗"
|
||||
},
|
||||
"features_panel": {
|
||||
"chat": "챗",
|
||||
"gopher": "고퍼",
|
||||
"media_proxy": "미디어 프록시",
|
||||
"scope_options": "범위 옵션",
|
||||
"text_limit": "텍스트 제한",
|
||||
"title": "기능",
|
||||
"who_to_follow": "팔로우 추천"
|
||||
},
|
||||
"finder": {
|
||||
"error_fetching_user": "사용자 정보 불러오기 실패",
|
||||
"find_user": "사용자 찾기"
|
||||
},
|
||||
"general": {
|
||||
"apply": "적용",
|
||||
"submit": "보내기"
|
||||
},
|
||||
"login": {
|
||||
"login": "로그인",
|
||||
"description": "OAuth로 로그인",
|
||||
"logout": "로그아웃",
|
||||
"password": "암호",
|
||||
"placeholder": "예시: lain",
|
||||
"register": "가입",
|
||||
"username": "사용자 이름"
|
||||
},
|
||||
"nav": {
|
||||
"about": "About",
|
||||
"back": "뒤로",
|
||||
"chat": "로컬 챗",
|
||||
"friend_requests": "팔로우 요청",
|
||||
"mentions": "멘션",
|
||||
"dms": "다이렉트 메시지",
|
||||
"public_tl": "공개 타임라인",
|
||||
"timeline": "타임라인",
|
||||
"twkn": "모든 알려진 네트워크",
|
||||
"user_search": "사용자 검색",
|
||||
"preferences": "환경설정"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "알 수 없는 게시물입니다, 검색 합니다...",
|
||||
"favorited_you": "당신의 게시물을 즐겨찾기",
|
||||
"followed_you": "당신을 팔로우",
|
||||
"load_older": "오래 된 알림 불러오기",
|
||||
"notifications": "알림",
|
||||
"read": "읽음!",
|
||||
"repeated_you": "당신의 게시물을 리핏"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "새 게시물 게시",
|
||||
"account_not_locked_warning": "당신의 계정은 {0} 상태가 아닙니다. 누구나 당신을 팔로우 하고 팔로워 전용 게시물을 볼 수 있습니다.",
|
||||
"account_not_locked_warning_link": "잠김",
|
||||
"attachments_sensitive": "첨부물을 민감함으로 설정",
|
||||
"content_type": {
|
||||
"plain_text": "평문"
|
||||
},
|
||||
"content_warning": "주제 (필수 아님)",
|
||||
"default": "LA에 도착!",
|
||||
"direct_warning": "이 게시물을 멘션 된 사용자들에게만 보여집니다",
|
||||
"posting": "게시",
|
||||
"scope": {
|
||||
"direct": "다이렉트 - 멘션 된 사용자들에게만",
|
||||
"private": "팔로워 전용 - 팔로워들에게만",
|
||||
"public": "공개 - 공개 타임라인으로",
|
||||
"unlisted": "비공개 - 공개 타임라인에 게시 안 함"
|
||||
}
|
||||
},
|
||||
"registration": {
|
||||
"bio": "소개",
|
||||
"email": "이메일",
|
||||
"fullname": "표시 되는 이름",
|
||||
"password_confirm": "암호 확인",
|
||||
"registration": "가입하기",
|
||||
"token": "초대 토큰",
|
||||
"captcha": "캡차",
|
||||
"new_captcha": "이미지를 클릭해서 새로운 캡차",
|
||||
"validations": {
|
||||
"username_required": "공백으로 둘 수 없습니다",
|
||||
"fullname_required": "공백으로 둘 수 없습니다",
|
||||
"email_required": "공백으로 둘 수 없습니다",
|
||||
"password_required": "공백으로 둘 수 없습니다",
|
||||
"password_confirmation_required": "공백으로 둘 수 없습니다",
|
||||
"password_confirmation_match": "패스워드와 일치해야 합니다"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"attachmentRadius": "첨부물",
|
||||
"attachments": "첨부물",
|
||||
"autoload": "최하단에 도착하면 자동으로 로드 활성화",
|
||||
"avatar": "아바타",
|
||||
"avatarAltRadius": "아바타 (알림)",
|
||||
"avatarRadius": "아바타",
|
||||
"background": "배경",
|
||||
"bio": "소개",
|
||||
"btnRadius": "버튼",
|
||||
"cBlue": "파랑 (답글, 팔로우)",
|
||||
"cGreen": "초록 (리트윗)",
|
||||
"cOrange": "주황 (즐겨찾기)",
|
||||
"cRed": "빨강 (취소)",
|
||||
"change_password": "암호 바꾸기",
|
||||
"change_password_error": "암호를 바꾸는 데 몇 가지 문제가 있습니다.",
|
||||
"changed_password": "암호를 바꾸었습니다!",
|
||||
"collapse_subject": "주제를 가진 게시물 접기",
|
||||
"composing": "작성",
|
||||
"confirm_new_password": "새 패스워드 확인",
|
||||
"current_avatar": "현재 아바타",
|
||||
"current_password": "현재 패스워드",
|
||||
"current_profile_banner": "현재 프로필 배너",
|
||||
"data_import_export_tab": "데이터 불러오기 / 내보내기",
|
||||
"default_vis": "기본 공개 범위",
|
||||
"delete_account": "계정 삭제",
|
||||
"delete_account_description": "계정과 메시지를 영구히 삭제.",
|
||||
"delete_account_error": "계정을 삭제하는데 문제가 있습니다. 계속 발생한다면 인스턴스 관리자에게 문의하세요.",
|
||||
"delete_account_instructions": "계정 삭제를 확인하기 위해 아래에 패스워드 입력.",
|
||||
"export_theme": "프리셋 저장",
|
||||
"filtering": "필터링",
|
||||
"filtering_explanation": "아래의 단어를 가진 게시물들은 뮤트 됩니다, 한 줄에 하나씩 적으세요",
|
||||
"follow_export": "팔로우 내보내기",
|
||||
"follow_export_button": "팔로우 목록을 csv로 내보내기",
|
||||
"follow_export_processing": "진행 중입니다, 곧 다운로드 가능해 질 것입니다",
|
||||
"follow_import": "팔로우 불러오기",
|
||||
"follow_import_error": "팔로우 불러오기 실패",
|
||||
"follows_imported": "팔로우 목록을 불러왔습니다! 처리에는 시간이 걸립니다.",
|
||||
"foreground": "전경",
|
||||
"general": "일반",
|
||||
"hide_attachments_in_convo": "대화의 첨부물 숨기기",
|
||||
"hide_attachments_in_tl": "타임라인의 첨부물 숨기기",
|
||||
"hide_isp": "인스턴스 전용 패널 숨기기",
|
||||
"preload_images": "이미지 미리 불러오기",
|
||||
"hide_post_stats": "게시물 통계 숨기기 (즐겨찾기 수 등)",
|
||||
"hide_user_stats": "사용자 통계 숨기기 (팔로워 수 등)",
|
||||
"import_followers_from_a_csv_file": "csv 파일에서 팔로우 목록 불러오기",
|
||||
"import_theme": "프리셋 불러오기",
|
||||
"inputRadius": "입력 칸",
|
||||
"checkboxRadius": "체크박스",
|
||||
"instance_default": "(기본: {value})",
|
||||
"instance_default_simple": "(기본)",
|
||||
"interface": "인터페이스",
|
||||
"interfaceLanguage": "인터페이스 언어",
|
||||
"invalid_theme_imported": "선택한 파일은 지원하는 플레로마 테마가 아닙니다. 아무런 변경도 일어나지 않았습니다.",
|
||||
"limited_availability": "이 브라우저에서 사용 불가",
|
||||
"links": "링크",
|
||||
"lock_account_description": "계정을 승인 된 팔로워들로 제한",
|
||||
"loop_video": "비디오 반복재생",
|
||||
"loop_video_silent_only": "소리가 없는 비디오만 반복 재생 (마스토돈의 \"gifs\" 같은 것들)",
|
||||
"name": "이름",
|
||||
"name_bio": "이름 & 소개",
|
||||
"new_password": "새 암호",
|
||||
"notification_visibility": "보여 줄 알림 종류",
|
||||
"notification_visibility_follows": "팔로우",
|
||||
"notification_visibility_likes": "좋아함",
|
||||
"notification_visibility_mentions": "멘션",
|
||||
"notification_visibility_repeats": "반복",
|
||||
"no_rich_text_description": "모든 게시물의 서식을 지우기",
|
||||
"hide_network_description": "내 팔로우와 팔로워를 숨기기",
|
||||
"nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화",
|
||||
"panelRadius": "패널",
|
||||
"pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기",
|
||||
"presets": "프리셋",
|
||||
"profile_background": "프로필 배경",
|
||||
"profile_banner": "프로필 배너",
|
||||
"profile_tab": "프로필",
|
||||
"radii_help": "인터페이스 모서리 둥글기 (픽셀 단위)",
|
||||
"replies_in_timeline": "답글을 타임라인에",
|
||||
"reply_link_preview": "마우스를 올려서 답글 링크 미리보기 활성화",
|
||||
"reply_visibility_all": "모든 답글 보기",
|
||||
"reply_visibility_following": "나에게 직접 오는 답글이나 내가 팔로우 중인 사람에게서 오는 답글만 표시",
|
||||
"reply_visibility_self": "나에게 직접 전송 된 답글만 보이기",
|
||||
"saving_err": "설정 저장 실패",
|
||||
"saving_ok": "설정 저장 됨",
|
||||
"security_tab": "보안",
|
||||
"scope_copy": "답글을 달 때 공개 범위 따라가리 (다이렉트 메시지는 언제나 따라감)",
|
||||
"set_new_avatar": "새 아바타 설정",
|
||||
"set_new_profile_background": "새 프로필 배경 설정",
|
||||
"set_new_profile_banner": "새 프로필 배너 설정",
|
||||
"settings": "설정",
|
||||
"subject_input_always_show": "항상 주제 칸 보이기",
|
||||
"subject_line_behavior": "답글을 달 때 주제 복사하기",
|
||||
"subject_line_email": "이메일처럼: \"re: 주제\"",
|
||||
"subject_line_mastodon": "마스토돈처럼: 그대로 복사",
|
||||
"subject_line_noop": "복사 안 함",
|
||||
"stop_gifs": "GIF파일에 마우스를 올려서 재생",
|
||||
"streaming": "최상단에 도달하면 자동으로 새 게시물 스트리밍",
|
||||
"text": "텍스트",
|
||||
"theme": "테마",
|
||||
"theme_help": "16진수 색상코드(#rrggbb)를 사용해 색상 테마를 커스터마이즈.",
|
||||
"theme_help_v2_1": "체크박스를 통해 몇몇 컴포넌트의 색상과 불투명도를 조절 가능, \"모두 지우기\" 버튼으로 덮어 씌운 것을 모두 취소.",
|
||||
"theme_help_v2_2": "몇몇 입력칸 밑의 아이콘은 전경/배경 대비 관련 표시등입니다, 마우스를 올려 자세한 정보를 볼 수 있습니다. 투명도 대비 표시등이 가장 최악의 경우를 나타낸다는 것을 유의하세요.",
|
||||
"tooltipRadius": "툴팁/경고",
|
||||
"user_settings": "사용자 설정",
|
||||
"values": {
|
||||
"false": "아니오",
|
||||
"true": "네"
|
||||
},
|
||||
"notifications": "알림",
|
||||
"enable_web_push_notifications": "웹 푸시 알림 활성화",
|
||||
"style": {
|
||||
"switcher": {
|
||||
"keep_color": "색상 유지",
|
||||
"keep_shadows": "그림자 유지",
|
||||
"keep_opacity": "불투명도 유지",
|
||||
"keep_roundness": "둥글기 유지",
|
||||
"keep_fonts": "글자체 유지",
|
||||
"save_load_hint": "\"유지\" 옵션들은 다른 테마를 고르거나 불러 올 때 현재 설정 된 옵션들을 건드리지 않게 합니다, 테마를 내보내기 할 때도 이 옵션에 따라 저장합니다. 아무 것도 체크 되지 않았다면 모든 설정을 내보냅니다.",
|
||||
"reset": "초기화",
|
||||
"clear_all": "모두 지우기",
|
||||
"clear_opacity": "불투명도 지우기"
|
||||
},
|
||||
"common": {
|
||||
"color": "색상",
|
||||
"opacity": "불투명도",
|
||||
"contrast": {
|
||||
"hint": "대비율이 {ratio}입니다, 이것은 {context} {level}",
|
||||
"level": {
|
||||
"aa": "AA등급 가이드라인에 부합합니다 (최소한도)",
|
||||
"aaa": "AAA등급 가이드라인에 부합합니다 (권장)",
|
||||
"bad": "아무런 가이드라인 등급에도 미치지 못합니다"
|
||||
},
|
||||
"context": {
|
||||
"18pt": "큰 (18pt 이상) 텍스트에 대해",
|
||||
"text": "텍스트에 대해"
|
||||
}
|
||||
}
|
||||
},
|
||||
"common_colors": {
|
||||
"_tab_label": "일반",
|
||||
"main": "일반 색상",
|
||||
"foreground_hint": "\"고급\" 탭에서 더 자세한 설정이 가능합니다",
|
||||
"rgbo": "아이콘, 강조, 배지"
|
||||
},
|
||||
"advanced_colors": {
|
||||
"_tab_label": "고급",
|
||||
"alert": "주의 배경",
|
||||
"alert_error": "에러",
|
||||
"badge": "배지 배경",
|
||||
"badge_notification": "알림",
|
||||
"panel_header": "패널 헤더",
|
||||
"top_bar": "상단 바",
|
||||
"borders": "테두리",
|
||||
"buttons": "버튼",
|
||||
"inputs": "입력칸",
|
||||
"faint_text": "흐려진 텍스트"
|
||||
},
|
||||
"radii": {
|
||||
"_tab_label": "둥글기"
|
||||
},
|
||||
"shadows": {
|
||||
"_tab_label": "그림자와 빛",
|
||||
"component": "컴포넌트",
|
||||
"override": "덮어쓰기",
|
||||
"shadow_id": "그림자 #{value}",
|
||||
"blur": "흐리기",
|
||||
"spread": "퍼지기",
|
||||
"inset": "안쪽으로",
|
||||
"hint": "그림자에는 CSS3 변수를 --variable을 통해 색상 값으로 사용할 수 있습니다. 불투명도에는 적용 되지 않습니다.",
|
||||
"filter_hint": {
|
||||
"always_drop_shadow": "경고, 이 그림자는 브라우저가 지원하는 경우 항상 {0}을 사용합니다.",
|
||||
"drop_shadow_syntax": "{0}는 {1} 파라미터와 {2} 키워드를 지원하지 않습니다.",
|
||||
"avatar_inset": "안쪽과 안쪽이 아닌 그림자를 모두 설정하는 경우 투명 아바타에서 예상치 못 한 결과가 나올 수 있다는 것에 주의해 주세요.",
|
||||
"spread_zero": "퍼지기가 0보다 큰 그림자는 0으로 설정한 것과 동일하게 보여집니다",
|
||||
"inset_classic": "안쪽 그림자는 {0}를 사용합니다"
|
||||
},
|
||||
"components": {
|
||||
"panel": "패널",
|
||||
"panelHeader": "패널 헤더",
|
||||
"topBar": "상단 바",
|
||||
"avatar": "사용자 아바타 (프로필 뷰에서)",
|
||||
"avatarStatus": "사용자 아바타 (게시물에서)",
|
||||
"popup": "팝업과 툴팁",
|
||||
"button": "버튼",
|
||||
"buttonHover": "버튼 (마우스 올렸을 때)",
|
||||
"buttonPressed": "버튼 (눌렸을 때)",
|
||||
"buttonPressedHover": "Button (마우스 올림 + 눌림)",
|
||||
"input": "입력칸"
|
||||
}
|
||||
},
|
||||
"fonts": {
|
||||
"_tab_label": "글자체",
|
||||
"help": "인터페이스의 요소에 사용 될 글자체를 고르세요. \"커스텀\"은 시스템에 있는 폰트 이름을 정확히 입력해야 합니다.",
|
||||
"components": {
|
||||
"interface": "인터페이스",
|
||||
"input": "입력칸",
|
||||
"post": "게시물 텍스트",
|
||||
"postCode": "게시물의 고정폭 텍스트 (서식 있는 텍스트)"
|
||||
},
|
||||
"family": "글자체 이름",
|
||||
"size": "크기 (px 단위)",
|
||||
"weight": "굵기",
|
||||
"custom": "커스텀"
|
||||
},
|
||||
"preview": {
|
||||
"header": "미리보기",
|
||||
"content": "내용",
|
||||
"error": "에러 예시",
|
||||
"button": "버튼",
|
||||
"text": "더 많은 {0} 그리고 {1}",
|
||||
"mono": "내용",
|
||||
"input": "LA에 막 도착!",
|
||||
"faint_link": "도움 되는 설명서",
|
||||
"fine_print": "우리의 {0} 를 읽고 도움 되지 않는 것들을 배우자!",
|
||||
"header_faint": "이건 괜찮아",
|
||||
"checkbox": "나는 약관을 대충 훑어보았습니다",
|
||||
"link": "작고 귀여운 링크"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "접기",
|
||||
"conversation": "대화",
|
||||
"error_fetching": "업데이트 불러오기 실패",
|
||||
"load_older": "더 오래 된 게시물 불러오기",
|
||||
"no_retweet_hint": "팔로워 전용, 다이렉트 메시지는 반복할 수 없습니다",
|
||||
"repeated": "반복 됨",
|
||||
"show_new": "새로운 것 보기",
|
||||
"up_to_date": "최신 상태"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "승인",
|
||||
"block": "차단",
|
||||
"blocked": "차단 됨!",
|
||||
"deny": "거부",
|
||||
"follow": "팔로우",
|
||||
"follow_sent": "요청 보내짐!",
|
||||
"follow_progress": "요청 중…",
|
||||
"follow_again": "요청을 다시 보낼까요?",
|
||||
"follow_unfollow": "팔로우 중지",
|
||||
"followees": "팔로우 중",
|
||||
"followers": "팔로워",
|
||||
"following": "팔로우 중!",
|
||||
"follows_you": "당신을 팔로우 합니다!",
|
||||
"its_you": "당신입니다!",
|
||||
"mute": "침묵",
|
||||
"muted": "침묵 됨",
|
||||
"per_day": " / 하루",
|
||||
"remote_follow": "원격 팔로우",
|
||||
"statuses": "게시물"
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "사용자 타임라인"
|
||||
},
|
||||
"who_to_follow": {
|
||||
"more": "더 보기",
|
||||
"who_to_follow": "팔로우 추천"
|
||||
},
|
||||
"tool_tip": {
|
||||
"media_upload": "미디어 업로드",
|
||||
"repeat": "반복",
|
||||
"reply": "답글",
|
||||
"favorite": "즐겨찾기",
|
||||
"user_settings": "사용자 설정"
|
||||
},
|
||||
"upload":{
|
||||
"error": {
|
||||
"base": "업로드 실패.",
|
||||
"file_too_big": "파일이 너무 커요 [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||
"default": "잠시 후에 다시 시도해 보세요"
|
||||
},
|
||||
"file_size_units": {
|
||||
"B": "바이트",
|
||||
"KiB": "키비바이트",
|
||||
"MiB": "메비바이트",
|
||||
"GiB": "기비바이트",
|
||||
"TiB": "테비바이트"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ const messages = {
|
|||
hu: require('./hu.json'),
|
||||
it: require('./it.json'),
|
||||
ja: require('./ja.json'),
|
||||
ko: require('./ko.json'),
|
||||
nb: require('./nb.json'),
|
||||
oc: require('./oc.json'),
|
||||
pl: require('./pl.json'),
|
||||
|
|
|
@ -279,6 +279,7 @@
|
|||
"user_card": {
|
||||
"block": "Заблокировать",
|
||||
"blocked": "Заблокирован",
|
||||
"favorites": "Понравившиеся",
|
||||
"follow": "Читать",
|
||||
"follow_sent": "Запрос отправлен!",
|
||||
"follow_progress": "Запрашиваем…",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { includes, remove, slice, sortBy, toInteger, each, find, flatten, maxBy, minBy, merge, last, isArray } from 'lodash'
|
||||
import { remove, slice, each, find, maxBy, minBy, merge, last, isArray } from 'lodash'
|
||||
import apiService from '../services/api/api.service.js'
|
||||
// import parse from '../services/status_parser/status_parser.js'
|
||||
|
||||
|
@ -36,6 +36,7 @@ export const defaultState = {
|
|||
mentions: emptyTl(),
|
||||
public: emptyTl(),
|
||||
user: emptyTl(),
|
||||
favorites: emptyTl(),
|
||||
publicAndExternal: emptyTl(),
|
||||
friends: emptyTl(),
|
||||
tag: emptyTl(),
|
||||
|
@ -43,20 +44,7 @@ export const defaultState = {
|
|||
}
|
||||
}
|
||||
|
||||
const isNsfw = (status) => {
|
||||
const nsfwRegex = /#nsfw/i
|
||||
return includes(status.tags, 'nsfw') || !!status.text.match(nsfwRegex)
|
||||
}
|
||||
|
||||
export const prepareStatus = (status) => {
|
||||
// Parse nsfw tags
|
||||
if (status.nsfw === undefined) {
|
||||
status.nsfw = isNsfw(status)
|
||||
if (status.retweeted_status) {
|
||||
status.nsfw = status.retweeted_status.nsfw
|
||||
}
|
||||
}
|
||||
|
||||
// Set deleted flag
|
||||
status.deleted = false
|
||||
|
||||
|
@ -75,35 +63,6 @@ const visibleNotificationTypes = (rootState) => {
|
|||
].filter(_ => _)
|
||||
}
|
||||
|
||||
export const statusType = (status) => {
|
||||
if (status.is_post_verb) {
|
||||
return 'status'
|
||||
}
|
||||
|
||||
if (status.retweeted_status) {
|
||||
return 'retweet'
|
||||
}
|
||||
|
||||
if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) ||
|
||||
(typeof status.text === 'string' && status.text.match(/favorited/))) {
|
||||
return 'favorite'
|
||||
}
|
||||
|
||||
if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) {
|
||||
return 'deletion'
|
||||
}
|
||||
|
||||
if (status.text.match(/started following/) || status.activity_type === 'follow') {
|
||||
return 'follow'
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
export const findMaxId = (...args) => {
|
||||
return (maxBy(flatten(args), 'id') || {}).id
|
||||
}
|
||||
|
||||
const mergeOrAdd = (arr, obj, item) => {
|
||||
const oldItem = obj[item.id]
|
||||
|
||||
|
@ -122,9 +81,11 @@ const mergeOrAdd = (arr, obj, item) => {
|
|||
}
|
||||
}
|
||||
|
||||
const sortById = (a, b) => a.id > b.id ? -1 : 1
|
||||
|
||||
const sortTimeline = (timeline) => {
|
||||
timeline.visibleStatuses = sortBy(timeline.visibleStatuses, ({id}) => -id)
|
||||
timeline.statuses = sortBy(timeline.statuses, ({id}) => -id)
|
||||
timeline.visibleStatuses = timeline.visibleStatuses.sort(sortById)
|
||||
timeline.statuses = timeline.statuses.sort(sortById)
|
||||
timeline.minVisibleId = (last(timeline.visibleStatuses) || {}).id
|
||||
return timeline
|
||||
}
|
||||
|
@ -153,13 +114,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
return
|
||||
}
|
||||
|
||||
const addStatus = (status, showImmediately, addToTimeline = true) => {
|
||||
const result = mergeOrAdd(allStatuses, allStatusesObject, status)
|
||||
status = result.item
|
||||
const addStatus = (data, showImmediately, addToTimeline = true) => {
|
||||
const result = mergeOrAdd(allStatuses, allStatusesObject, data)
|
||||
const status = result.item
|
||||
|
||||
if (result.new) {
|
||||
// We are mentioned in a post
|
||||
if (statusType(status) === 'status' && find(status.attentions, { id: user.id })) {
|
||||
if (status.type === 'status' && find(status.attentions, { id: user.id })) {
|
||||
const mentions = state.timelines.mentions
|
||||
|
||||
// Add the mention to the mentions timeline
|
||||
|
@ -200,7 +161,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
}
|
||||
|
||||
const favoriteStatus = (favorite, counter) => {
|
||||
const status = find(allStatuses, { id: toInteger(favorite.in_reply_to_status_id) })
|
||||
const status = find(allStatuses, { id: favorite.in_reply_to_status_id })
|
||||
if (status) {
|
||||
// This is our favorite, so the relevant bit.
|
||||
if (favorite.user.id === user.id) {
|
||||
|
@ -263,6 +224,9 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
remove(timelineObject.visibleStatuses, { uri })
|
||||
}
|
||||
},
|
||||
'follow': (follow) => {
|
||||
// NOOP, it is known status but we don't do anything about it for now
|
||||
},
|
||||
'default': (unknown) => {
|
||||
console.log('unknown status type')
|
||||
console.log(unknown)
|
||||
|
@ -270,7 +234,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
}
|
||||
|
||||
each(statuses, (status) => {
|
||||
const type = statusType(status)
|
||||
const type = status.type
|
||||
const processor = processors[type] || processors['default']
|
||||
processor(status)
|
||||
})
|
||||
|
@ -288,42 +252,36 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
|||
const allStatuses = state.allStatuses
|
||||
const allStatusesObject = state.allStatusesObject
|
||||
each(notifications, (notification) => {
|
||||
const result = mergeOrAdd(allStatuses, allStatusesObject, notification.notice)
|
||||
const action = result.item
|
||||
notification.action = mergeOrAdd(allStatuses, allStatusesObject, notification.action).item
|
||||
notification.status = notification.status && mergeOrAdd(allStatuses, allStatusesObject, notification.status).item
|
||||
|
||||
// Only add a new notification if we don't have one for the same action
|
||||
if (!find(state.notifications.data, (oldNotification) => oldNotification.action.id === action.id)) {
|
||||
state.notifications.maxId = Math.max(notification.id, state.notifications.maxId)
|
||||
state.notifications.minId = Math.min(notification.id, state.notifications.minId)
|
||||
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
|
||||
state.notifications.maxId = notification.id > state.notifications.maxId
|
||||
? notification.id
|
||||
: state.notifications.maxId
|
||||
state.notifications.minId = notification.id < state.notifications.minId
|
||||
? notification.id
|
||||
: state.notifications.minId
|
||||
|
||||
const fresh = !notification.is_seen
|
||||
const status = notification.ntype === 'like'
|
||||
? action.favorited_status
|
||||
: action
|
||||
|
||||
const result = {
|
||||
type: notification.ntype,
|
||||
status,
|
||||
action,
|
||||
seen: !fresh
|
||||
}
|
||||
|
||||
state.notifications.data.push(result)
|
||||
state.notifications.idStore[notification.id] = result
|
||||
state.notifications.data.push(notification)
|
||||
state.notifications.idStore[notification.id] = notification
|
||||
|
||||
if ('Notification' in window && window.Notification.permission === 'granted') {
|
||||
const notifObj = {}
|
||||
const action = notification.action
|
||||
const title = action.user.name
|
||||
const result = {}
|
||||
result.icon = action.user.profile_image_url
|
||||
result.body = action.text // there's a problem that it doesn't put a space before links tho
|
||||
notifObj.icon = action.user.profile_image_url
|
||||
notifObj.body = action.text // there's a problem that it doesn't put a space before links tho
|
||||
|
||||
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
|
||||
if (action.attachments && action.attachments.length > 0 && !action.nsfw &&
|
||||
action.attachments[0].mimetype.startsWith('image/')) {
|
||||
result.image = action.attachments[0].url
|
||||
notifObj.image = action.attachments[0].url
|
||||
}
|
||||
|
||||
if (fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) {
|
||||
let notification = new window.Notification(title, result)
|
||||
if (notification.fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) {
|
||||
let notification = new window.Notification(title, notifObj)
|
||||
// Chrome is known for not closing notifications automatically
|
||||
// according to MDN, anyway.
|
||||
setTimeout(notification.close.bind(notification), 5000)
|
||||
|
|
|
@ -68,6 +68,7 @@ export const mutations = {
|
|||
},
|
||||
setUserForNotification (state, notification) {
|
||||
notification.action.user = state.usersObject[notification.action.user.id]
|
||||
notification.from_profile = state.usersObject[notification.action.user.id]
|
||||
},
|
||||
setColor (state, { user: { id }, highlighted }) {
|
||||
const user = state.usersObject[id]
|
||||
|
@ -149,8 +150,8 @@ const users = {
|
|||
})
|
||||
},
|
||||
addNewNotifications (store, { notifications }) {
|
||||
const users = compact(map(notifications, 'from_profile'))
|
||||
const notificationIds = compact(notifications.map(_ => String(_.id)))
|
||||
const users = map(notifications, 'from_profile')
|
||||
const notificationIds = notifications.map(_ => _.id)
|
||||
store.commit('addNewUsers', users)
|
||||
|
||||
const notificationsObject = store.rootState.statuses.notifications.idStore
|
||||
|
@ -206,39 +207,38 @@ const users = {
|
|||
const commit = store.commit
|
||||
commit('beginLogin')
|
||||
store.rootState.api.backendInteractor.verifyCredentials(accessToken)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
response.json()
|
||||
.then((user) => {
|
||||
// user.credentials = userCredentials
|
||||
user.credentials = accessToken
|
||||
commit('setCurrentUser', user)
|
||||
commit('addNewUsers', [user])
|
||||
.then((data) => {
|
||||
if (!data.error) {
|
||||
const user = data
|
||||
// user.credentials = userCredentials
|
||||
user.credentials = accessToken
|
||||
commit('setCurrentUser', user)
|
||||
commit('addNewUsers', [user])
|
||||
|
||||
getNotificationPermission()
|
||||
.then(permission => commit('setNotificationPermission', permission))
|
||||
getNotificationPermission()
|
||||
.then(permission => commit('setNotificationPermission', permission))
|
||||
|
||||
// Set our new backend interactor
|
||||
commit('setBackendInteractor', backendInteractorService(accessToken))
|
||||
// Set our new backend interactor
|
||||
commit('setBackendInteractor', backendInteractorService(accessToken))
|
||||
|
||||
if (user.token) {
|
||||
store.dispatch('initializeSocket', user.token)
|
||||
}
|
||||
if (user.token) {
|
||||
store.dispatch('initializeSocket', user.token)
|
||||
}
|
||||
|
||||
// Start getting fresh tweets.
|
||||
store.dispatch('startFetching', 'friends')
|
||||
// Start getting fresh tweets.
|
||||
store.dispatch('startFetching', 'friends')
|
||||
|
||||
// Get user mutes and follower info
|
||||
store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
|
||||
each(mutedUsers, (user) => { user.muted = true })
|
||||
store.commit('addNewUsers', mutedUsers)
|
||||
})
|
||||
// Get user mutes and follower info
|
||||
store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
|
||||
each(mutedUsers, (user) => { user.muted = true })
|
||||
store.commit('addNewUsers', mutedUsers)
|
||||
})
|
||||
|
||||
// Fetch our friends
|
||||
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
||||
.then((friends) => commit('addNewUsers', friends))
|
||||
})
|
||||
// Fetch our friends
|
||||
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
||||
.then((friends) => commit('addNewUsers', friends))
|
||||
} else {
|
||||
const response = data.error
|
||||
// Authentication failed
|
||||
commit('endLogin')
|
||||
if (response.status === 401) {
|
||||
|
@ -250,11 +250,11 @@ const users = {
|
|||
commit('endLogin')
|
||||
resolve()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
commit('endLogin')
|
||||
reject('Failed to connect to server, try again')
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
commit('endLogin')
|
||||
reject('Failed to connect to server, try again')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,10 @@ const APPROVE_USER_URL = '/api/pleroma/friendships/approve'
|
|||
const DENY_USER_URL = '/api/pleroma/friendships/deny'
|
||||
const SUGGESTIONS_URL = '/api/v1/suggestions'
|
||||
|
||||
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
|
||||
|
||||
import { each, map } from 'lodash'
|
||||
import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
import 'whatwg-fetch'
|
||||
|
||||
const oldfetch = window.fetch
|
||||
|
@ -70,6 +73,7 @@ const updateAvatar = ({credentials, params}) => {
|
|||
form.append(key, value)
|
||||
}
|
||||
})
|
||||
|
||||
return fetch(url, {
|
||||
headers: authHeaders(credentials),
|
||||
method: 'POST',
|
||||
|
@ -87,6 +91,7 @@ const updateBg = ({credentials, params}) => {
|
|||
form.append(key, value)
|
||||
}
|
||||
})
|
||||
|
||||
return fetch(url, {
|
||||
headers: authHeaders(credentials),
|
||||
method: 'POST',
|
||||
|
@ -110,6 +115,7 @@ const updateBanner = ({credentials, params}) => {
|
|||
form.append(key, value)
|
||||
}
|
||||
})
|
||||
|
||||
return fetch(url, {
|
||||
headers: authHeaders(credentials),
|
||||
method: 'POST',
|
||||
|
@ -237,24 +243,28 @@ const fetchUser = ({id, credentials}) => {
|
|||
let url = `${USER_URL}?user_id=${id}`
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((data) => data.json())
|
||||
.then((data) => parseUser(data))
|
||||
}
|
||||
|
||||
const fetchFriends = ({id, credentials}) => {
|
||||
let url = `${FRIENDS_URL}?user_id=${id}`
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((data) => data.json())
|
||||
.then((data) => data.map(parseUser))
|
||||
}
|
||||
|
||||
const fetchFollowers = ({id, credentials}) => {
|
||||
let url = `${FOLLOWERS_URL}?user_id=${id}`
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((data) => data.json())
|
||||
.then((data) => data.map(parseUser))
|
||||
}
|
||||
|
||||
const fetchAllFollowing = ({username, credentials}) => {
|
||||
const url = `${ALL_FOLLOWING_URL}/${username}.json`
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((data) => data.json())
|
||||
.then((data) => data.map(parseUser))
|
||||
}
|
||||
|
||||
const fetchFollowRequests = ({credentials}) => {
|
||||
|
@ -266,13 +276,27 @@ const fetchFollowRequests = ({credentials}) => {
|
|||
const fetchConversation = ({id, credentials}) => {
|
||||
let url = `${CONVERSATION_URL}/${id}.json?count=100`
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((data) => {
|
||||
if (data.ok) {
|
||||
return data
|
||||
}
|
||||
throw new Error('Error fetching timeline', data)
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.then((data) => data.map(parseStatus))
|
||||
}
|
||||
|
||||
const fetchStatus = ({id, credentials}) => {
|
||||
let url = `${STATUS_URL}/${id}.json`
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((data) => {
|
||||
if (data.ok) {
|
||||
return data
|
||||
}
|
||||
throw new Error('Error fetching timeline', data)
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.then((data) => parseStatus(data))
|
||||
}
|
||||
|
||||
const setUserMute = ({id, credentials, muted = true}) => {
|
||||
|
@ -300,13 +324,14 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
|
|||
notifications: QVITTER_USER_NOTIFICATIONS_URL,
|
||||
'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL,
|
||||
user: QVITTER_USER_TIMELINE_URL,
|
||||
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
|
||||
tag: TAG_TIMELINE_URL
|
||||
}
|
||||
const isNotifications = timeline === 'notifications'
|
||||
const params = []
|
||||
|
||||
let url = timelineUrls[timeline]
|
||||
|
||||
let params = []
|
||||
|
||||
if (since) {
|
||||
params.push(['since_id', since])
|
||||
}
|
||||
|
@ -330,9 +355,10 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
|
|||
if (data.ok) {
|
||||
return data
|
||||
}
|
||||
throw new Error('Error fetching timeline')
|
||||
throw new Error('Error fetching timeline', data)
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.then((data) => data.map(isNotifications ? parseNotification : parseStatus))
|
||||
}
|
||||
|
||||
const verifyCredentials = (user) => {
|
||||
|
@ -340,6 +366,16 @@ const verifyCredentials = (user) => {
|
|||
method: 'POST',
|
||||
headers: authHeaders(user)
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json()
|
||||
} else {
|
||||
return {
|
||||
error: response
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((data) => data.error ? data : parseUser(data))
|
||||
}
|
||||
|
||||
const favorite = ({ id, credentials }) => {
|
||||
|
@ -390,6 +426,16 @@ const postStatus = ({credentials, status, spoilerText, visibility, sensitive, me
|
|||
method: 'POST',
|
||||
headers: authHeaders(credentials)
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json()
|
||||
} else {
|
||||
return {
|
||||
error: response
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((data) => data.error ? data : parseStatus(data))
|
||||
}
|
||||
|
||||
const deleteStatus = ({ id, credentials }) => {
|
||||
|
|
263
src/services/entity_normalizer/entity_normalizer.service.js
Normal file
263
src/services/entity_normalizer/entity_normalizer.service.js
Normal file
|
@ -0,0 +1,263 @@
|
|||
const qvitterStatusType = (status) => {
|
||||
if (status.is_post_verb) {
|
||||
return 'status'
|
||||
}
|
||||
|
||||
if (status.retweeted_status) {
|
||||
return 'retweet'
|
||||
}
|
||||
|
||||
if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) ||
|
||||
(typeof status.text === 'string' && status.text.match(/favorited/))) {
|
||||
return 'favorite'
|
||||
}
|
||||
|
||||
if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) {
|
||||
return 'deletion'
|
||||
}
|
||||
|
||||
if (status.text.match(/started following/) || status.activity_type === 'follow') {
|
||||
return 'follow'
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
export const parseUser = (data) => {
|
||||
const output = {}
|
||||
const masto = data.hasOwnProperty('acct')
|
||||
// case for users in "mentions" property for statuses in MastoAPI
|
||||
const mastoShort = masto && !data.hasOwnProperty('avatar')
|
||||
|
||||
output.id = String(data.id)
|
||||
|
||||
if (masto) {
|
||||
output.screen_name = data.acct
|
||||
|
||||
// There's nothing else to get
|
||||
if (mastoShort) {
|
||||
return output
|
||||
}
|
||||
|
||||
output.name = null // missing
|
||||
output.name_html = data.display_name
|
||||
|
||||
output.description = null // missing
|
||||
output.description_html = data.note
|
||||
|
||||
// Utilize avatar_static for gif avatars?
|
||||
output.profile_image_url = data.avatar
|
||||
output.profile_image_url_original = data.avatar
|
||||
|
||||
// Same, utilize header_static?
|
||||
output.cover_photo = data.header
|
||||
|
||||
output.friends_count = data.following_count
|
||||
|
||||
output.bot = data.bot
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Missing, trying to recover
|
||||
output.is_local = !output.screen_name.includes('@')
|
||||
} else {
|
||||
output.screen_name = data.screen_name
|
||||
|
||||
output.name = data.name
|
||||
output.name_html = data.name_html
|
||||
|
||||
output.description = data.description
|
||||
output.description_html = data.description_html
|
||||
|
||||
output.profile_image_url = data.profile_image_url
|
||||
output.profile_image_url_original = data.profile_image_url_original
|
||||
|
||||
output.cover_photo = data.cover_photo
|
||||
|
||||
output.friends_count = data.friends_count
|
||||
|
||||
output.bot = null // missing
|
||||
|
||||
output.statusnet_profile_url = data.statusnet_profile_url
|
||||
|
||||
output.statusnet_blocking = data.statusnet_blocking
|
||||
|
||||
output.is_local = data.is_local
|
||||
|
||||
output.follows_you = data.follows_you
|
||||
|
||||
output.muted = data.muted
|
||||
|
||||
// QVITTER ONLY FOR NOW
|
||||
// Really only applies to logged in user, really.. I THINK
|
||||
output.rights = data.rights
|
||||
output.no_rich_text = data.no_rich_text
|
||||
output.default_scope = data.default_scope
|
||||
output.hide_network = data.hide_network
|
||||
output.background_image = data.background_image
|
||||
}
|
||||
|
||||
output.created_at = new Date(data.created_at)
|
||||
output.locked = data.locked
|
||||
output.followers_count = data.followers_count
|
||||
output.statuses_count = data.statuses_count
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
const parseAttachment = (data) => {
|
||||
const output = {}
|
||||
const masto = !data.hasOwnProperty('oembed')
|
||||
|
||||
if (masto) {
|
||||
// Not exactly same...
|
||||
output.mimetype = data.type
|
||||
output.meta = data.meta // not present in BE yet
|
||||
} else {
|
||||
output.mimetype = data.mimetype
|
||||
output.meta = null // missing
|
||||
}
|
||||
|
||||
output.url = data.url
|
||||
output.description = data.description
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
export const parseStatus = (data) => {
|
||||
const output = {}
|
||||
const masto = data.hasOwnProperty('account')
|
||||
|
||||
if (masto) {
|
||||
output.favorited = data.favourited
|
||||
output.fave_num = data.favourites_count
|
||||
|
||||
output.repeated = data.reblogged
|
||||
output.repeat_num = data.reblogs_count
|
||||
|
||||
output.type = data.reblog ? 'retweet' : 'status'
|
||||
output.nsfw = data.sensitive
|
||||
|
||||
output.statusnet_html = data.content
|
||||
|
||||
// Not exactly the same but works?
|
||||
output.text = data.content
|
||||
|
||||
output.in_reply_to_status_id = data.in_reply_to_id
|
||||
output.in_reply_to_user_id = data.in_reply_to_account_id
|
||||
|
||||
// Missing!! fix in UI?
|
||||
output.in_reply_to_screen_name = null
|
||||
|
||||
// Not exactly the same but works
|
||||
output.statusnet_conversation_id = data.id
|
||||
|
||||
if (output.type === 'retweet') {
|
||||
output.retweeted_status = parseStatus(data.reblog)
|
||||
}
|
||||
|
||||
output.summary = data.spoiler_text
|
||||
output.external_url = data.url
|
||||
|
||||
// FIXME missing!!
|
||||
output.is_local = false
|
||||
} else {
|
||||
output.favorited = data.favorited
|
||||
output.fave_num = data.fave_num
|
||||
|
||||
output.repeated = data.repeated
|
||||
output.repeat_num = data.repeat_num
|
||||
|
||||
// catchall, temporary
|
||||
// Object.assign(output, data)
|
||||
|
||||
output.type = qvitterStatusType(data)
|
||||
|
||||
if (data.nsfw === undefined) {
|
||||
output.nsfw = isNsfw(data)
|
||||
if (data.retweeted_status) {
|
||||
output.nsfw = data.retweeted_status.nsfw
|
||||
}
|
||||
} else {
|
||||
output.nsfw = data.nsfw
|
||||
}
|
||||
|
||||
output.statusnet_html = data.statusnet_html
|
||||
output.text = data.text
|
||||
|
||||
output.in_reply_to_status_id = data.in_reply_to_status_id
|
||||
output.in_reply_to_user_id = data.in_reply_to_user_id
|
||||
output.in_reply_to_screen_name = data.in_reply_to_screen_name
|
||||
|
||||
output.statusnet_conversation_id = data.statusnet_conversation_id
|
||||
|
||||
if (output.type === 'retweet') {
|
||||
output.retweeted_status = parseStatus(data.retweeted_status)
|
||||
}
|
||||
|
||||
output.summary = data.summary
|
||||
output.external_url = data.external_url
|
||||
output.is_local = data.is_local
|
||||
}
|
||||
|
||||
output.id = String(data.id)
|
||||
output.visibility = data.visibility
|
||||
output.created_at = new Date(data.created_at)
|
||||
|
||||
output.user = parseUser(masto ? data.account : data.user)
|
||||
|
||||
output.attentions = ((masto ? data.mentions : data.attentions) || []).map(parseUser)
|
||||
|
||||
output.attachments = ((masto ? data.media_attachments : data.attachments) || [])
|
||||
.map(parseAttachment)
|
||||
|
||||
const retweetedStatus = masto ? data.reblog : data.retweeted_status
|
||||
if (retweetedStatus) {
|
||||
output.retweeted_status = parseStatus(retweetedStatus)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
export const parseNotification = (data) => {
|
||||
const mastoDict = {
|
||||
'favourite': 'like',
|
||||
'reblog': 'repeat'
|
||||
}
|
||||
const masto = !data.hasOwnProperty('ntype')
|
||||
const output = {}
|
||||
|
||||
if (masto) {
|
||||
output.type = mastoDict[data.type] || data.type
|
||||
output.seen = null // missing
|
||||
output.status = parseStatus(data.status)
|
||||
output.action = output.status // not sure
|
||||
output.from_profile = parseUser(data.account)
|
||||
} else {
|
||||
const parsedNotice = parseStatus(data.notice)
|
||||
output.type = data.ntype
|
||||
output.seen = Boolean(data.is_seen)
|
||||
output.status = output.type === 'like'
|
||||
? parseStatus(data.notice.favorited_status)
|
||||
: parsedNotice
|
||||
output.action = parsedNotice
|
||||
output.from_profile = parseUser(data.from_profile)
|
||||
}
|
||||
|
||||
output.created_at = new Date(data.created_at)
|
||||
output.id = String(data.id)
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
const isNsfw = (status) => {
|
||||
const nsfwRegex = /#nsfw/i
|
||||
return (status.tags || []).includes('nsfw') || !!status.text.match(nsfwRegex)
|
||||
}
|
|
@ -10,8 +10,8 @@ export const visibleTypes = store => ([
|
|||
].filter(_ => _))
|
||||
|
||||
export const visibleNotificationsFromStore = store => {
|
||||
// Don't know why, but sortBy([seen, -action.id]) doesn't work.
|
||||
let sortedNotifications = sortBy(notificationsFromStore(store), ({action}) => -action.id)
|
||||
// map is just to clone the array since sort mutates it and it causes some issues
|
||||
let sortedNotifications = notificationsFromStore(store).map(_ => _).sort((a, b) => a.action.id > b.action.id ? -1 : 1)
|
||||
sortedNotifications = sortBy(sortedNotifications, 'seen')
|
||||
return sortedNotifications.filter((notification) => visibleTypes(store).includes(notification.type))
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ const postStatus = ({ store, status, spoilerText, visibility, sensitive, media =
|
|||
const mediaIds = map(media, 'id')
|
||||
|
||||
return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType, noAttachmentLinks: store.state.instance.noAttachmentLinks})
|
||||
.then((data) => data.json())
|
||||
.then((data) => {
|
||||
if (!data.error) {
|
||||
store.dispatch('addNewStatuses', {
|
||||
|
|
|
@ -31,7 +31,7 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
|
|||
|
||||
return apiService.fetchTimeline(args)
|
||||
.then((statuses) => {
|
||||
if (!older && statuses.length >= 20 && !timelineData.loading) {
|
||||
if (!older && statuses.length >= 20 && !timelineData.loading && timelineData.statuses.length) {
|
||||
store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId })
|
||||
}
|
||||
update({store, statuses, timeline, showImmediately, userId})
|
||||
|
|
BIN
static/logo.png
BIN
static/logo.png
Binary file not shown.
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.3 KiB |
1582
test/fixtures/mastoapi.json
vendored
Normal file
1582
test/fixtures/mastoapi.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,76 +1,31 @@
|
|||
import { cloneDeep } from 'lodash'
|
||||
import { defaultState, mutations, findMaxId, prepareStatus, statusType } from '../../../../src/modules/statuses.js'
|
||||
import { defaultState, mutations, prepareStatus } from '../../../../src/modules/statuses.js'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
const makeMockStatus = ({id, text, is_post_verb = true}) => {
|
||||
const makeMockStatus = ({id, text, type = 'status'}) => {
|
||||
return {
|
||||
id,
|
||||
user: {id: 0},
|
||||
user: {id: '0'},
|
||||
name: 'status',
|
||||
text: text || `Text number ${id}`,
|
||||
fave_num: 0,
|
||||
uri: '',
|
||||
is_post_verb,
|
||||
type,
|
||||
attentions: []
|
||||
}
|
||||
}
|
||||
|
||||
describe('Statuses.statusType', () => {
|
||||
it('identifies favorites', () => {
|
||||
const fav = {
|
||||
uri: 'tag:soykaf.com,2016-08-21:fave:2558:note:339495:2016-08-21T16:54:04+00:00'
|
||||
}
|
||||
|
||||
const mastoFav = {
|
||||
uri: 'tag:mastodon.social,2016-11-27:objectId=73903:objectType=Favourite'
|
||||
}
|
||||
|
||||
expect(statusType(fav)).to.eql('favorite')
|
||||
expect(statusType(mastoFav)).to.eql('favorite')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Statuses.prepareStatus', () => {
|
||||
it('sets nsfw for statuses with the #nsfw tag', () => {
|
||||
const safe = makeMockStatus({id: 1, text: 'Hello oniichan'})
|
||||
const nsfw = makeMockStatus({id: 1, text: 'Hello oniichan #nsfw'})
|
||||
|
||||
expect(prepareStatus(safe).nsfw).to.eq(false)
|
||||
expect(prepareStatus(nsfw).nsfw).to.eq(true)
|
||||
})
|
||||
|
||||
it('leaves existing nsfw settings alone', () => {
|
||||
const nsfw = makeMockStatus({id: 1, text: 'Hello oniichan #nsfw'})
|
||||
nsfw.nsfw = false
|
||||
|
||||
expect(prepareStatus(nsfw).nsfw).to.eq(false)
|
||||
})
|
||||
|
||||
it('sets deleted flag to false', () => {
|
||||
const aStatus = makeMockStatus({id: 1, text: 'Hello oniichan'})
|
||||
const aStatus = makeMockStatus({id: '1', text: 'Hello oniichan'})
|
||||
expect(prepareStatus(aStatus).deleted).to.eq(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Statuses.findMaxId', () => {
|
||||
it('returns the largest id in any of the given arrays', () => {
|
||||
const statusesOne = [{ id: 100 }, { id: 2 }]
|
||||
const statusesTwo = [{ id: 3 }]
|
||||
|
||||
const maxId = findMaxId(statusesOne, statusesTwo)
|
||||
expect(maxId).to.eq(100)
|
||||
})
|
||||
|
||||
it('returns undefined for empty arrays', () => {
|
||||
const maxId = findMaxId([], [])
|
||||
expect(maxId).to.eq(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('The Statuses module', () => {
|
||||
it('adds the status to allStatuses and to the given timeline', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const status = makeMockStatus({id: 1})
|
||||
const status = makeMockStatus({id: '1'})
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' })
|
||||
|
||||
|
@ -82,7 +37,7 @@ describe('The Statuses module', () => {
|
|||
|
||||
it('counts the status as new if it has not been seen on this timeline', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const status = makeMockStatus({id: 1})
|
||||
const status = makeMockStatus({id: '1'})
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' })
|
||||
mutations.addNewStatuses(state, { statuses: [status], timeline: 'friends' })
|
||||
|
@ -100,7 +55,7 @@ describe('The Statuses module', () => {
|
|||
|
||||
it('add the statuses to allStatuses if no timeline is given', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const status = makeMockStatus({id: 1})
|
||||
const status = makeMockStatus({id: '1'})
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status] })
|
||||
|
||||
|
@ -112,7 +67,7 @@ describe('The Statuses module', () => {
|
|||
|
||||
it('adds the status to allStatuses and to the given timeline, directly visible', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const status = makeMockStatus({id: 1})
|
||||
const status = makeMockStatus({id: '1'})
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
|
||||
|
@ -124,10 +79,10 @@ describe('The Statuses module', () => {
|
|||
|
||||
it('removes statuses by tag on deletion', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const status = makeMockStatus({id: 1})
|
||||
const otherStatus = makeMockStatus({id: 3})
|
||||
const status = makeMockStatus({id: '1'})
|
||||
const otherStatus = makeMockStatus({id: '3'})
|
||||
status.uri = 'xxx'
|
||||
const deletion = makeMockStatus({id: 2, is_post_verb: false})
|
||||
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'
|
||||
|
||||
|
@ -137,36 +92,36 @@ describe('The Statuses module', () => {
|
|||
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)
|
||||
expect(state.timelines.public.maxId).to.eql('3')
|
||||
})
|
||||
|
||||
it('does not update the maxId when the noIdUpdate flag is set', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const status = makeMockStatus({id: 1})
|
||||
const secondStatus = makeMockStatus({id: 2})
|
||||
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)
|
||||
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)
|
||||
expect(state.timelines.public.maxId).to.eql('1')
|
||||
})
|
||||
|
||||
it('keeps a descending by id order in timeline.visibleStatuses and timeline.statuses', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const nonVisibleStatus = makeMockStatus({id: 1})
|
||||
const status = makeMockStatus({id: 3})
|
||||
const statusTwo = makeMockStatus({id: 2})
|
||||
const statusThree = makeMockStatus({id: 4})
|
||||
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)
|
||||
expect(state.timelines.public.minVisibleId).to.equal('2')
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [statusThree], showImmediately: true, timeline: 'public' })
|
||||
|
||||
|
@ -176,9 +131,9 @@ describe('The Statuses module', () => {
|
|||
|
||||
it('splits retweets from their status and links them', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const status = makeMockStatus({id: 1})
|
||||
const retweet = makeMockStatus({id: 2, is_post_verb: false})
|
||||
const modStatus = makeMockStatus({id: 1, text: 'something else'})
|
||||
const status = makeMockStatus({id: '1'})
|
||||
const retweet = makeMockStatus({id: '2', type: 'retweet'})
|
||||
const modStatus = makeMockStatus({id: '1', text: 'something else'})
|
||||
|
||||
retweet.retweeted_status = status
|
||||
|
||||
|
@ -187,22 +142,22 @@ describe('The Statuses module', () => {
|
|||
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)
|
||||
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].id).to.equal('1')
|
||||
expect(state.allStatuses[0].text).to.equal(modStatus.text)
|
||||
expect(state.allStatuses[1].id).to.equal(2)
|
||||
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 = cloneDeep(defaultState)
|
||||
const status = makeMockStatus({id: 1})
|
||||
const modStatus = makeMockStatus({id: 1, text: 'something else'})
|
||||
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' })
|
||||
|
@ -218,9 +173,9 @@ describe('The Statuses module', () => {
|
|||
|
||||
it('replaces existing statuses with the same id, coming from a retweet', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const status = makeMockStatus({id: 1})
|
||||
const modStatus = makeMockStatus({id: 1, text: 'something else'})
|
||||
const retweet = makeMockStatus({id: 2, is_post_verb: false})
|
||||
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
|
||||
|
@ -239,15 +194,15 @@ describe('The Statuses module', () => {
|
|||
|
||||
it('handles favorite actions', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const status = makeMockStatus({id: 1})
|
||||
const status = makeMockStatus({id: '1'})
|
||||
|
||||
const favorite = {
|
||||
id: 2,
|
||||
is_post_verb: false,
|
||||
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 }
|
||||
user: { id: '99' }
|
||||
}
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
|
@ -266,12 +221,12 @@ describe('The Statuses module', () => {
|
|||
|
||||
// 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
|
||||
id: '1'
|
||||
}
|
||||
|
||||
const ownFavorite = {
|
||||
id: 3,
|
||||
is_post_verb: false,
|
||||
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',
|
||||
|
@ -287,16 +242,16 @@ describe('The Statuses module', () => {
|
|||
|
||||
describe('notifications', () => {
|
||||
it('removes a notification when the notice gets removed', () => {
|
||||
const user = { id: 1 }
|
||||
const user = { id: '1' }
|
||||
const state = cloneDeep(defaultState)
|
||||
const status = makeMockStatus({id: 1})
|
||||
const otherStatus = makeMockStatus({id: 3})
|
||||
const mentionedStatus = makeMockStatus({id: 2})
|
||||
const status = makeMockStatus({id: '1'})
|
||||
const otherStatus = makeMockStatus({id: '3'})
|
||||
const mentionedStatus = makeMockStatus({id: '2'})
|
||||
mentionedStatus.attentions = [user]
|
||||
mentionedStatus.uri = 'xxx'
|
||||
otherStatus.attentions = [user]
|
||||
|
||||
const deletion = makeMockStatus({id: 4, is_post_verb: false})
|
||||
const deletion = makeMockStatus({id: '4', type: 'deletion'})
|
||||
deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
|
||||
deletion.uri = 'xxx'
|
||||
|
||||
|
@ -305,10 +260,12 @@ describe('The Statuses module', () => {
|
|||
state,
|
||||
{
|
||||
notifications: [{
|
||||
ntype: 'mention',
|
||||
from_profile: { id: '2' },
|
||||
id: '998',
|
||||
type: 'mention',
|
||||
status: otherStatus,
|
||||
notice: otherStatus,
|
||||
is_seen: false
|
||||
action: otherStatus,
|
||||
seen: false
|
||||
}]
|
||||
})
|
||||
|
||||
|
@ -317,10 +274,12 @@ describe('The Statuses module', () => {
|
|||
state,
|
||||
{
|
||||
notifications: [{
|
||||
ntype: 'mention',
|
||||
from_profile: { id: '2' },
|
||||
id: '999',
|
||||
type: 'mention',
|
||||
status: mentionedStatus,
|
||||
notice: mentionedStatus,
|
||||
is_seen: false
|
||||
action: mentionedStatus,
|
||||
seen: false
|
||||
}]
|
||||
})
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ describe('The users module', () => {
|
|||
describe('mutations', () => {
|
||||
it('adds new users to the set, merging in new information for old users', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const user = { id: 1, name: 'Guy' }
|
||||
const modUser = { id: 1, name: 'Dude' }
|
||||
const user = { id: '1', name: 'Guy' }
|
||||
const modUser = { id: '1', name: 'Dude' }
|
||||
|
||||
mutations.addNewUsers(state, [user])
|
||||
expect(state.users).to.have.length(1)
|
||||
|
@ -21,7 +21,7 @@ describe('The users module', () => {
|
|||
|
||||
it('sets a mute bit on users', () => {
|
||||
const state = cloneDeep(defaultState)
|
||||
const user = { id: 1, name: 'Guy' }
|
||||
const user = { id: '1', name: 'Guy' }
|
||||
|
||||
mutations.addNewUsers(state, [user])
|
||||
mutations.setMuted(state, {user, muted: true})
|
||||
|
@ -38,11 +38,11 @@ describe('The users module', () => {
|
|||
it('returns user with matching screen_name', () => {
|
||||
const state = {
|
||||
users: [
|
||||
{ screen_name: 'Guy', id: 1 }
|
||||
{ screen_name: 'Guy', id: '1' }
|
||||
]
|
||||
}
|
||||
const name = 'Guy'
|
||||
const expected = { screen_name: 'Guy', id: 1 }
|
||||
const expected = { screen_name: 'Guy', id: '1' }
|
||||
expect(getters.userByName(state)(name)).to.eql(expected)
|
||||
})
|
||||
})
|
||||
|
@ -51,11 +51,11 @@ describe('The users module', () => {
|
|||
it('returns user with matching id', () => {
|
||||
const state = {
|
||||
users: [
|
||||
{ screen_name: 'Guy', id: 1 }
|
||||
{ screen_name: 'Guy', id: '1' }
|
||||
]
|
||||
}
|
||||
const id = 1
|
||||
const expected = { screen_name: 'Guy', id: 1 }
|
||||
const id = '1'
|
||||
const expected = { screen_name: 'Guy', id: '1' }
|
||||
expect(getters.userById(state)(id)).to.eql(expected)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,270 @@
|
|||
import { parseStatus, parseUser, parseNotification } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
|
||||
import mastoapidata from '../../../../fixtures/mastoapi.json'
|
||||
import qvitterapidata from '../../../../fixtures/statuses.json'
|
||||
|
||||
const makeMockStatusQvitter = (overrides = {}) => {
|
||||
return Object.assign({
|
||||
activity_type: 'post',
|
||||
attachments: [],
|
||||
attentions: [],
|
||||
created_at: 'Tue Jan 15 13:57:56 +0000 2019',
|
||||
external_url: 'https://ap.example/whatever',
|
||||
fave_num: 1,
|
||||
favorited: false,
|
||||
id: '10335970',
|
||||
in_reply_to_ostatus_uri: null,
|
||||
in_reply_to_profileurl: null,
|
||||
in_reply_to_screen_name: null,
|
||||
in_reply_to_status_id: null,
|
||||
in_reply_to_user_id: null,
|
||||
is_local: false,
|
||||
is_post_verb: true,
|
||||
possibly_sensitive: false,
|
||||
repeat_num: 0,
|
||||
repeated: false,
|
||||
statusnet_conversation_id: '16300488',
|
||||
statusnet_html: '<p>haha benis</p>',
|
||||
summary: null,
|
||||
tags: [],
|
||||
text: 'haha benis',
|
||||
uri: 'https://ap.example/whatever',
|
||||
user: makeMockUserQvitter(),
|
||||
visibility: 'public'
|
||||
}, overrides)
|
||||
}
|
||||
|
||||
const makeMockUserQvitter = (overrides = {}) => {
|
||||
return Object.assign({
|
||||
background_image: null,
|
||||
cover_photo: '',
|
||||
created_at: 'Mon Jan 14 13:56:51 +0000 2019',
|
||||
default_scope: 'public',
|
||||
description: 'ebin',
|
||||
description_html: '<p>ebin</p>',
|
||||
favourites_count: 0,
|
||||
fields: [],
|
||||
followers_count: 1,
|
||||
following: true,
|
||||
follows_you: true,
|
||||
friends_count: 1,
|
||||
id: '60717',
|
||||
is_local: false,
|
||||
locked: false,
|
||||
name: 'Spurdo :ebin:',
|
||||
name_html: 'Spurdo <img src="whatever">',
|
||||
no_rich_text: false,
|
||||
pleroma: { confirmation_pending: false, tags: [] },
|
||||
profile_image_url: 'https://ap.example/whatever',
|
||||
profile_image_url_https: 'https://ap.example/whatever',
|
||||
profile_image_url_original: 'https://ap.example/whatever',
|
||||
profile_image_url_profile_size: 'https://ap.example/whatever',
|
||||
rights: { delete_others_notice: false },
|
||||
screen_name: 'spurdo@ap.example',
|
||||
statuses_count: 46,
|
||||
statusnet_blocking: false,
|
||||
statusnet_profile_url: ''
|
||||
}, overrides)
|
||||
}
|
||||
|
||||
const makeMockUserMasto = (overrides = {}) => {
|
||||
return Object.assign({
|
||||
acct: 'hj',
|
||||
avatar:
|
||||
'https://shigusegubu.club/media/1657b945-8d5b-4ce6-aafb-4c3fc5772120/8ce851029af84d55de9164e30cc7f46d60cbf12eee7e96c5c0d35d9038ddade1.png',
|
||||
avatar_static:
|
||||
'https://shigusegubu.club/media/1657b945-8d5b-4ce6-aafb-4c3fc5772120/8ce851029af84d55de9164e30cc7f46d60cbf12eee7e96c5c0d35d9038ddade1.png',
|
||||
bot: false,
|
||||
created_at: '2017-12-17T21:54:14.000Z',
|
||||
display_name: 'whatever whatever whatever witch',
|
||||
emojis: [],
|
||||
fields: [],
|
||||
followers_count: 705,
|
||||
following_count: 326,
|
||||
header:
|
||||
'https://shigusegubu.club/media/7ab024d9-2a8a-4fbc-9ce8-da06756ae2db/6aadefe4e264133bc377ab450e6b045b6f5458542a5c59e6c741f86107f0388b.png',
|
||||
header_static:
|
||||
'https://shigusegubu.club/media/7ab024d9-2a8a-4fbc-9ce8-da06756ae2db/6aadefe4e264133bc377ab450e6b045b6f5458542a5c59e6c741f86107f0388b.png',
|
||||
id: '1',
|
||||
locked: false,
|
||||
note:
|
||||
'Volatile Internet Weirdo. Name pronounced as Hee Jay. JS and Java dark arts mage, Elixir trainee. I love sampo and lain. Matrix is <span><a data-user="1" href="https://shigusegubu.club/users/hj">@<span>hj</span></a></span>:matrix.heldscal.la Pronouns are whatever. Do not DM me unless it\'s truly private matter and you\'re instance\'s admin or you risk your DM to be reposted publicly.Wish i was Finnish girl.',
|
||||
pleroma: { confirmation_pending: false, tags: null },
|
||||
source: { note: '', privacy: 'public', sensitive: false },
|
||||
statuses_count: 41775,
|
||||
url: 'https://shigusegubu.club/users/hj',
|
||||
username: 'hj'
|
||||
}, overrides)
|
||||
}
|
||||
|
||||
const makeMockStatusMasto = (overrides = {}) => {
|
||||
return Object.assign({
|
||||
account: makeMockUserMasto(),
|
||||
application: { name: 'Web', website: null },
|
||||
content:
|
||||
'<span><a data-user="14660" href="https://pleroma.soykaf.com/users/sampo">@<span>sampo</span></a></span> god i wish i was there',
|
||||
created_at: '2019-01-17T16:29:23.000Z',
|
||||
emojis: [],
|
||||
favourited: false,
|
||||
favourites_count: 1,
|
||||
id: '10423476',
|
||||
in_reply_to_account_id: '14660',
|
||||
in_reply_to_id: '10423197',
|
||||
language: null,
|
||||
media_attachments: [],
|
||||
mentions: [
|
||||
{
|
||||
acct: 'sampo@pleroma.soykaf.com',
|
||||
id: '14660',
|
||||
url: 'https://pleroma.soykaf.com/users/sampo',
|
||||
username: 'sampo'
|
||||
}
|
||||
],
|
||||
muted: false,
|
||||
reblog: null,
|
||||
reblogged: false,
|
||||
reblogs_count: 0,
|
||||
replies_count: 0,
|
||||
sensitive: false,
|
||||
spoiler_text: '',
|
||||
tags: [],
|
||||
uri: 'https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639',
|
||||
url: 'https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639',
|
||||
visibility: 'public'
|
||||
}, overrides)
|
||||
}
|
||||
|
||||
const makeMockNotificationQvitter = (overrides = {}) => {
|
||||
return Object.assign({
|
||||
notice: makeMockStatusQvitter(),
|
||||
ntype: 'follow',
|
||||
from_profile: makeMockUserQvitter(),
|
||||
is_seen: 0,
|
||||
id: 123
|
||||
}, overrides)
|
||||
}
|
||||
|
||||
parseNotification
|
||||
parseUser
|
||||
parseStatus
|
||||
makeMockStatusQvitter
|
||||
makeMockUserQvitter
|
||||
|
||||
describe('API Entities normalizer', () => {
|
||||
describe('parseStatus', () => {
|
||||
describe('QVitter preprocessing', () => {
|
||||
it('doesn\'t blow up', () => {
|
||||
const parsed = qvitterapidata.map(parseStatus)
|
||||
expect(parsed.length).to.eq(qvitterapidata.length)
|
||||
})
|
||||
|
||||
it('identifies favorites', () => {
|
||||
const fav = {
|
||||
uri: 'tag:soykaf.com,2016-08-21:fave:2558:note:339495:2016-08-21T16:54:04+00:00',
|
||||
is_post_verb: false
|
||||
}
|
||||
|
||||
const mastoFav = {
|
||||
uri: 'tag:mastodon.social,2016-11-27:objectId=73903:objectType=Favourite',
|
||||
is_post_verb: false
|
||||
}
|
||||
|
||||
expect(parseStatus(makeMockStatusQvitter(fav))).to.have.property('type', 'favorite')
|
||||
expect(parseStatus(makeMockStatusQvitter(mastoFav))).to.have.property('type', 'favorite')
|
||||
})
|
||||
|
||||
it('processes repeats correctly', () => {
|
||||
const post = makeMockStatusQvitter({ retweeted_status: null, id: 'deadbeef' })
|
||||
const repeat = makeMockStatusQvitter({ retweeted_status: post, is_post_verb: false, id: 'foobar' })
|
||||
|
||||
const parsedPost = parseStatus(post)
|
||||
const parsedRepeat = parseStatus(repeat)
|
||||
|
||||
expect(parsedPost).to.have.property('type', 'status')
|
||||
expect(parsedRepeat).to.have.property('type', 'retweet')
|
||||
expect(parsedRepeat).to.have.property('retweeted_status')
|
||||
expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef')
|
||||
})
|
||||
|
||||
it('sets nsfw for statuses with the #nsfw tag', () => {
|
||||
const safe = makeMockStatusQvitter({id: '1', text: 'Hello oniichan'})
|
||||
const nsfw = makeMockStatusQvitter({id: '1', text: 'Hello oniichan #nsfw'})
|
||||
|
||||
expect(parseStatus(safe).nsfw).to.eq(false)
|
||||
expect(parseStatus(nsfw).nsfw).to.eq(true)
|
||||
})
|
||||
|
||||
it('leaves existing nsfw settings alone', () => {
|
||||
const nsfw = makeMockStatusQvitter({id: '1', text: 'Hello oniichan #nsfw', nsfw: false})
|
||||
|
||||
expect(parseStatus(nsfw).nsfw).to.eq(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Mastoapi preprocessing and converting', () => {
|
||||
it('doesn\'t blow up', () => {
|
||||
const parsed = mastoapidata.map(parseStatus)
|
||||
expect(parsed.length).to.eq(mastoapidata.length)
|
||||
})
|
||||
|
||||
it('processes repeats correctly', () => {
|
||||
const post = makeMockStatusMasto({ reblog: null, id: 'deadbeef' })
|
||||
const repeat = makeMockStatusMasto({ reblog: post, id: 'foobar' })
|
||||
|
||||
const parsedPost = parseStatus(post)
|
||||
const parsedRepeat = parseStatus(repeat)
|
||||
|
||||
expect(parsedPost).to.have.property('type', 'status')
|
||||
expect(parsedRepeat).to.have.property('type', 'retweet')
|
||||
expect(parsedRepeat).to.have.property('retweeted_status')
|
||||
expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Statuses generally already contain some info regarding users and there's nearly 1:1 mapping, so very little to test
|
||||
describe('parseUsers (MastoAPI)', () => {
|
||||
it('sets correct is_local for users depending on their screen_name', () => {
|
||||
const local = makeMockUserMasto({ acct: 'foo' })
|
||||
const remote = makeMockUserMasto({ acct: 'foo@bar.baz' })
|
||||
|
||||
expect(parseUser(local)).to.have.property('is_local', true)
|
||||
expect(parseUser(remote)).to.have.property('is_local', false)
|
||||
})
|
||||
})
|
||||
|
||||
// We currently use QvitterAPI notifications only, and especially due to MastoAPI lacking is_seen, support for MastoAPI
|
||||
// is more of an afterthought
|
||||
describe('parseNotifications (QvitterAPI)', () => {
|
||||
it('correctly normalizes data to FE\'s format', () => {
|
||||
const notif = makeMockNotificationQvitter({
|
||||
id: 123,
|
||||
notice: makeMockStatusQvitter({ id: 444 }),
|
||||
from_profile: makeMockUserQvitter({ id: 'spurdo' })
|
||||
})
|
||||
expect(parseNotification(notif)).to.have.property('id', '123')
|
||||
expect(parseNotification(notif)).to.have.property('seen', false)
|
||||
expect(parseNotification(notif)).to.have.deep.property('status.id', '444')
|
||||
expect(parseNotification(notif)).to.have.deep.property('action.id', '444')
|
||||
expect(parseNotification(notif)).to.have.deep.property('from_profile.id', 'spurdo')
|
||||
})
|
||||
|
||||
it('correctly normalizes favorite notifications', () => {
|
||||
const notif = makeMockNotificationQvitter({
|
||||
id: 123,
|
||||
ntype: 'like',
|
||||
notice: makeMockStatusQvitter({
|
||||
id: 444,
|
||||
favorited_status: makeMockStatusQvitter({ id: 4412 })
|
||||
}),
|
||||
is_seen: 1,
|
||||
from_profile: makeMockUserQvitter({ id: 'spurdo' })
|
||||
})
|
||||
expect(parseNotification(notif)).to.have.property('id', '123')
|
||||
expect(parseNotification(notif)).to.have.property('type', 'like')
|
||||
expect(parseNotification(notif)).to.have.property('seen', true)
|
||||
expect(parseNotification(notif)).to.have.deep.property('status.id', '4412')
|
||||
expect(parseNotification(notif)).to.have.deep.property('action.id', '444')
|
||||
expect(parseNotification(notif)).to.have.deep.property('from_profile.id', 'spurdo')
|
||||
})
|
||||
})
|
||||
})
|
|
@ -9,15 +9,15 @@ describe('NotificationUtils', () => {
|
|||
notifications: {
|
||||
data: [
|
||||
{
|
||||
action: { id: 1 },
|
||||
action: { id: '1' },
|
||||
type: 'like'
|
||||
},
|
||||
{
|
||||
action: { id: 2 },
|
||||
action: { id: '2' },
|
||||
type: 'mention'
|
||||
},
|
||||
{
|
||||
action: { id: 3 },
|
||||
action: { id: '3' },
|
||||
type: 'repeat'
|
||||
}
|
||||
]
|
||||
|
@ -34,11 +34,11 @@ describe('NotificationUtils', () => {
|
|||
}
|
||||
const expected = [
|
||||
{
|
||||
action: { id: 3 },
|
||||
action: { id: '3' },
|
||||
type: 'repeat'
|
||||
},
|
||||
{
|
||||
action: { id: 1 },
|
||||
action: { id: '1' },
|
||||
type: 'like'
|
||||
}
|
||||
]
|
||||
|
@ -54,12 +54,12 @@ describe('NotificationUtils', () => {
|
|||
notifications: {
|
||||
data: [
|
||||
{
|
||||
action: { id: 1 },
|
||||
action: { id: '1' },
|
||||
type: 'like',
|
||||
seen: false
|
||||
},
|
||||
{
|
||||
action: { id: 2 },
|
||||
action: { id: '2' },
|
||||
type: 'mention',
|
||||
seen: true
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ describe('NotificationUtils', () => {
|
|||
}
|
||||
const expected = [
|
||||
{
|
||||
action: { id: 1 },
|
||||
action: { id: '1' },
|
||||
type: 'like',
|
||||
seen: false
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue