Merge branch 'develop' into feature/mobile-improvements-2

This commit is contained in:
shpuld 2019-03-02 14:58:23 +02:00
commit b0cf148545
58 changed files with 597 additions and 431 deletions

View file

@ -1,26 +1,22 @@
<template> <template>
<div class="user-card"> <div class="user-card">
<router-link :to="userProfileLink(user)"> <router-link :to="userProfileLink(user)">
<UserAvatar class="avatar" :compact="true" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/> <UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
</router-link> </router-link>
<div class="user-card-expanded-content" v-if="userExpanded"> <div class="user-card-expanded-content" v-if="userExpanded">
<user-card-content :user="user" :switcher="false"></user-card-content> <user-card-content :user="user" :switcher="false"></user-card-content>
</div> </div>
<div class="user-card-collapsed-content" v-else> <div class="user-card-collapsed-content" v-else>
<div class="user-card-primary-area"> <div :title="user.name" class="user-card-user-name">
<div :title="user.name" class="user-name"> <span v-if="user.name_html" v-html="user.name_html"></span>
<span v-if="user.name_html" v-html="user.name_html"></span> <span v-else>{{ user.name }}</span>
<span v-else>{{ user.name }}</span>
</div>
<div>
<router-link class='user-screen-name' :to="userProfileLink(user)">
@{{user.screen_name}}
</router-link>
</div>
</div> </div>
<div class="user-card-secondary-area"> <div>
<slot name="secondary-area"></slot> <router-link class="user-card-screen-name" :to="userProfileLink(user)">
@{{user.screen_name}}
</router-link>
</div> </div>
<slot></slot>
</div> </div>
</div> </div>
</template> </template>
@ -46,30 +42,21 @@
margin-left: 0.7em; margin-left: 0.7em;
text-align: left; text-align: left;
flex: 1; flex: 1;
display: flex; min-width: 0;
align-items: flex-start;
justify-content: space-between;
} }
&-primary-area { &-user-name {
flex: 1; img {
.user-name { object-fit: contain;
img { height: 16px;
object-fit: contain; width: 16px;
height: 16px; vertical-align: middle;
width: 16px;
vertical-align: middle;
}
} }
} }
&-secondary-area {
flex: none;
}
&-expanded-content { &-expanded-content {
flex: 1; flex: 1;
margin: 0.2em 0 0 0.7em; margin-left: 0.7em;
border-radius: $fallback--panelRadius; border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius); border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid; border-style: solid;

View file

@ -1,6 +1,6 @@
<template> <template>
<basic-user-card :user="user"> <basic-user-card :user="user">
<template slot="secondary-area"> <div class="block-card-content-container">
<button class="btn btn-default" @click="unblockUser" :disabled="progress" v-if="blocked"> <button class="btn btn-default" @click="unblockUser" :disabled="progress" v-if="blocked">
<template v-if="progress"> <template v-if="progress">
{{ $t('user_card.unblock_progress') }} {{ $t('user_card.unblock_progress') }}
@ -17,8 +17,18 @@
{{ $t('user_card.block') }} {{ $t('user_card.block') }}
</template> </template>
</button> </button>
</template> </div>
</basic-user-card> </basic-user-card>
</template> </template>
<script src="./block_card.js"></script> <script src="./block_card.js"></script>
<style lang="scss">
.block-card-content-container {
margin-top: 0.5em;
text-align: right;
button {
width: 10em;
}
}
</style>

View file

@ -0,0 +1,45 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
const FollowCard = {
props: [
'user',
'noFollowsYou'
],
data () {
return {
inProgress: false,
requestSent: false,
updated: false
}
},
components: {
BasicUserCard
},
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
}
},
methods: {
followUser () {
this.inProgress = true
requestFollow(this.user, this.$store).then(({ sent, updated }) => {
this.inProgress = false
this.requestSent = sent
this.updated = updated
})
},
unfollowUser () {
this.inProgress = true
requestUnfollow(this.user, this.$store).then(({ updated }) => {
this.inProgress = false
this.updated = updated
})
}
}
}
export default FollowCard

View file

@ -0,0 +1,53 @@
<template>
<basic-user-card :user="user">
<div class="follow-card-content-container">
<span class="faint" v-if="!noFollowsYou && user.follows_you">
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span>
<button
v-if="showFollow"
class="btn btn-default"
@click="followUser"
:disabled="inProgress"
:title="requestSent ? $t('user_card.follow_again') : ''"
>
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else-if="requestSent">
{{ $t('user_card.follow_sent') }}
</template>
<template v-else>
{{ $t('user_card.follow') }}
</template>
</button>
<button v-if="following" class="btn btn-default pressed" @click="unfollowUser" :disabled="inProgress">
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else>
{{ $t('user_card.follow_unfollow') }}
</template>
</button>
</div>
</basic-user-card>
</template>
<script src="./follow_card.js"></script>
<style lang="scss">
.follow-card-content-container {
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
line-height: 1.5em;
.btn {
margin-top: 0.5em;
margin-left: auto;
width: 10em;
}
}
</style>

View file

@ -1,68 +0,0 @@
import UserCard from '../user_card/user_card.vue'
const FollowList = {
data () {
return {
loading: false,
bottomedOut: false,
error: false
}
},
props: ['userId', 'showFollowers'],
created () {
window.addEventListener('scroll', this.scrollLoad)
if (this.entries.length === 0) {
this.fetchEntries()
}
},
destroyed () {
window.removeEventListener('scroll', this.scrollLoad)
this.$store.dispatch('clearFriendsAndFollowers', this.userId)
},
computed: {
user () {
return this.$store.getters.userById(this.userId)
},
entries () {
return this.showFollowers ? this.user.followers : this.user.friends
},
showFollowsYou () {
return !this.showFollowers || (this.showFollowers && this.userId !== this.$store.state.users.currentUser.id)
}
},
methods: {
fetchEntries () {
if (!this.loading) {
const command = this.showFollowers ? 'addFollowers' : 'addFriends'
this.loading = true
this.$store.dispatch(command, this.userId).then(entries => {
this.error = false
this.loading = false
this.bottomedOut = entries.length === 0
}).catch(() => {
this.error = true
this.loading = false
})
}
},
scrollLoad (e) {
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -(bodyBRect.y))
if (this.loading === false &&
this.bottomedOut === false &&
this.$el.offsetHeight > 0 &&
(window.innerHeight + window.pageYOffset) >= (height - 750)
) {
this.fetchEntries()
}
}
},
watch: {
'user': 'fetchEntries'
},
components: {
UserCard
}
}
export default FollowList

View file

@ -1,33 +0,0 @@
<template>
<div class="follow-list">
<user-card
v-for="entry in entries"
:key="entry.id" :user="entry"
:noFollowsYou="!showFollowsYou"
/>
<div class="text-center panel-footer">
<a v-if="error" @click="fetchEntries" class="alert error">
{{$t('general.generic_error')}}
</a>
<i v-else-if="loading" class="icon-spin3 animate-spin"/>
<span v-else-if="bottomedOut"></span>
<a v-else @click="fetchEntries">{{$t('general.more')}}</a>
</div>
</div>
</template>
<script src="./follow_list.js"></script>
<style lang="scss">
.follow-list {
.panel-footer {
padding: 10px;
}
.error {
font-size: 14px;
}
}
</style>

View file

@ -0,0 +1,20 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const FollowRequestCard = {
props: ['user'],
components: {
BasicUserCard
},
methods: {
approveUser () {
this.$store.state.api.backendInteractor.approveUser(this.user.id)
this.$store.dispatch('removeFollowRequest', this.user)
},
denyUser () {
this.$store.state.api.backendInteractor.denyUser(this.user.id)
this.$store.dispatch('removeFollowRequest', this.user)
}
}
}
export default FollowRequestCard

View file

@ -0,0 +1,29 @@
<template>
<basic-user-card :user="user">
<div class="follow-request-card-content-container">
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
<button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button>
</div>
</basic-user-card>
</template>
<script src="./follow_request_card.js"></script>
<style lang="scss">
.follow-request-card-content-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
button {
margin-top: 0.5em;
margin-right: 0.5em;
flex: 1 1;
max-width: 12em;
min-width: 8em;
&:last-child {
margin-right: 0;
}
}
}
</style>

View file

@ -1,22 +1,13 @@
import UserCard from '../user_card/user_card.vue' import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
const FollowRequests = { const FollowRequests = {
components: { components: {
UserCard FollowRequestCard
},
created () {
this.updateRequests()
}, },
computed: { computed: {
requests () { requests () {
return this.$store.state.api.followRequests return this.$store.state.api.followRequests
} }
},
methods: {
updateRequests () {
this.$store.state.api.backendInteractor.fetchFollowRequests()
.then((requests) => { this.$store.commit('setFollowRequests', requests) })
}
} }
} }

View file

@ -4,7 +4,7 @@
{{$t('nav.friend_requests')}} {{$t('nav.friend_requests')}}
</div> </div>
<div class="panel-body"> <div class="panel-body">
<user-card v-for="request in requests" :key="request.id" :user="request" :showFollows="false" :showApproval="true"></user-card> <FollowRequestCard v-for="request in requests" :key="request.id" :user="request"/>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,10 +1,23 @@
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
const NavPanel = { const NavPanel = {
created () {
if (this.currentUser && this.currentUser.locked) {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
followRequestFetcher.startFetching({ store, credentials })
}
},
computed: { computed: {
currentUser () { currentUser () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
}, },
chat () { chat () {
return this.$store.state.chat.channel return this.$store.state.chat.channel
},
followRequestCount () {
return this.$store.state.api.followRequests.length
} }
} }
} }

View file

@ -20,8 +20,8 @@
<li v-if='currentUser && currentUser.locked'> <li v-if='currentUser && currentUser.locked'>
<router-link :to="{ name: 'friend-requests' }"> <router-link :to="{ name: 'friend-requests' }">
{{ $t("nav.friend_requests")}} {{ $t("nav.friend_requests")}}
<span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count"> <span v-if='followRequestCount > 0' class="badge follow-request-count">
{{currentUser.follow_request_count}} {{followRequestCount}}
</span> </span>
</router-link> </router-link>
</li> </li>

View file

@ -12,6 +12,7 @@ const settings = {
return { return {
hideAttachmentsLocal: user.hideAttachments, hideAttachmentsLocal: user.hideAttachments,
hideAttachmentsInConvLocal: user.hideAttachmentsInConv, hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
maxThumbnails: user.maxThumbnails,
hideNsfwLocal: user.hideNsfw, hideNsfwLocal: user.hideNsfw,
useOneClickNsfw: user.useOneClickNsfw, useOneClickNsfw: user.useOneClickNsfw,
hideISPLocal: user.hideISP, hideISPLocal: user.hideISP,
@ -186,6 +187,10 @@ const settings = {
}, },
useContainFit (value) { useContainFit (value) {
this.$store.dispatch('setOption', { name: 'useContainFit', value }) this.$store.dispatch('setOption', { name: 'useContainFit', value })
},
maxThumbnails (value) {
value = this.maxThumbnails = Math.floor(Math.max(value, 0))
this.$store.dispatch('setOption', { name: 'maxThumbnails', value })
} }
} }
} }

View file

@ -136,6 +136,10 @@
<input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal"> <input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal">
<label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label> <label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label>
</li> </li>
<li>
<label for="maxThumbnails">{{$t('settings.max_thumbnails')}}</label>
<input class="number-input" type="number" id="maxThumbnails" v-model.number="maxThumbnails" min="0" step="1">
</li>
<li> <li>
<input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal"> <input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
<label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label> <label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
@ -146,7 +150,7 @@
<label for="preloadImage">{{$t('settings.preload_images')}}</label> <label for="preloadImage">{{$t('settings.preload_images')}}</label>
</li> </li>
<li> <li>
<input type="checkbox" id="useOneClickNsfw" v-model="useOneClickNsfw"> <input :disabled="!hideNsfwLocal" type="checkbox" id="useOneClickNsfw" v-model="useOneClickNsfw">
<label for="useOneClickNsfw">{{$t('settings.use_one_click_nsfw')}}</label> <label for="useOneClickNsfw">{{$t('settings.use_one_click_nsfw')}}</label>
</li> </li>
</ul> </ul>
@ -316,6 +320,10 @@
min-width: 10em; min-width: 10em;
padding: 0 2em; padding: 0 2em;
} }
.number-input {
max-width: 6em;
}
} }
.select-multiple { .select-multiple {
display: flex; display: flex;

View file

@ -32,6 +32,9 @@ const SideDrawer = {
}, },
sitename () { sitename () {
return this.$store.state.instance.name return this.$store.state.instance.name
},
followRequestCount () {
return this.$store.state.api.followRequests.length
} }
}, },
methods: { methods: {

View file

@ -40,8 +40,8 @@
<li v-if="currentUser && currentUser.locked" @click="toggleDrawer"> <li v-if="currentUser && currentUser.locked" @click="toggleDrawer">
<router-link to='/friend-requests'> <router-link to='/friend-requests'>
{{ $t("nav.friend_requests") }} {{ $t("nav.friend_requests") }}
<span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count"> <span v-if='followRequestCount > 0' class="badge follow-request-count">
{{currentUser.follow_request_count}} {{followRequestCount}}
</span> </span>
</router-link> </router-link>

View file

@ -40,8 +40,7 @@ const Status = {
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined' expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
? !this.$store.state.instance.collapseMessageWithSubject ? !this.$store.state.instance.collapseMessageWithSubject
: !this.$store.state.config.collapseMessageWithSubject, : !this.$store.state.config.collapseMessageWithSubject,
betterShadow: this.$store.state.interface.browserSupport.cssFilter, betterShadow: this.$store.state.interface.browserSupport.cssFilter
maxAttachments: 9
} }
}, },
computed: { computed: {
@ -225,7 +224,7 @@ const Status = {
attachmentSize () { attachmentSize () {
if ((this.$store.state.config.hideAttachments && !this.inConversation) || if ((this.$store.state.config.hideAttachments && !this.inConversation) ||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation) || (this.$store.state.config.hideAttachmentsInConv && this.inConversation) ||
(this.status.attachments.length > this.maxAttachments)) { (this.status.attachments.length > this.maxThumbnails)) {
return 'hide' return 'hide'
} else if (this.compact) { } else if (this.compact) {
return 'small' return 'small'
@ -249,6 +248,9 @@ const Status = {
return this.status.attachments.filter( return this.status.attachments.filter(
file => !fileType.fileMatchesSomeType(this.galleryTypes, file) file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
) )
},
maxThumbnails () {
return this.$store.state.config.maxThumbnails
} }
}, },
components: { components: {

View file

@ -1,7 +1,6 @@
import Status from '../status/status.vue' import Status from '../status/status.vue'
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js' import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue' import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue'
import UserCard from '../user_card/user_card.vue'
import { throttle } from 'lodash' import { throttle } from 'lodash'
const Timeline = { const Timeline = {
@ -44,8 +43,7 @@ const Timeline = {
}, },
components: { components: {
Status, Status,
StatusOrConversation, StatusOrConversation
UserCard
}, },
created () { created () {
const store = this.$store const store = this.$store
@ -70,14 +68,21 @@ const Timeline = {
document.addEventListener('visibilitychange', this.handleVisibilityChange, false) document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
this.unfocused = document.hidden this.unfocused = document.hidden
} }
window.addEventListener('keydown', this.handleShortKey)
}, },
destroyed () { destroyed () {
window.removeEventListener('scroll', this.scrollLoad) window.removeEventListener('scroll', this.scrollLoad)
window.removeEventListener('keydown', this.handleShortKey)
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false) if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
this.$store.commit('setLoading', { timeline: this.timelineName, value: false }) this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
}, },
methods: { methods: {
handleShortKey (e) {
if (e.key === '.') this.showNewStatuses()
},
showNewStatuses () { showNewStatuses () {
if (this.newStatusCount === 0) return
if (this.timeline.flushMarker !== 0) { if (this.timeline.flushMarker !== 0) {
this.$store.commit('clearTimeline', { timeline: this.timelineName }) this.$store.commit('clearTimeline', { timeline: this.timelineName })
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 }) this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
@ -101,7 +106,7 @@ const Timeline = {
tag: this.tag tag: this.tag
}).then(statuses => { }).then(statuses => {
store.commit('setLoading', { timeline: this.timelineName, value: false }) store.commit('setLoading', { timeline: this.timelineName, value: false })
if (statuses.length === 0) { if (statuses && statuses.length === 0) {
this.bottomedOut = true this.bottomedOut = true
} }
}) })

View file

@ -1,64 +0,0 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
const UserCard = {
props: [
'user',
'noFollowsYou',
'showApproval'
],
data () {
return {
userExpanded: false,
followRequestInProgress: false,
followRequestSent: false,
updated: false
}
},
components: {
UserCardContent,
UserAvatar
},
computed: {
currentUser () { return this.$store.state.users.currentUser },
following () { return this.updated ? this.updated.following : this.user.following },
showFollow () {
return !this.showApproval && (!this.following || this.updated && !this.updated.following)
}
},
methods: {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
approveUser () {
this.$store.state.api.backendInteractor.approveUser(this.user.id)
this.$store.dispatch('removeFollowRequest', this.user)
},
denyUser () {
this.$store.state.api.backendInteractor.denyUser(this.user.id)
this.$store.dispatch('removeFollowRequest', this.user)
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
},
followUser () {
this.followRequestInProgress = true
requestFollow(this.user, this.$store).then(({ sent, updated }) => {
this.followRequestInProgress = false
this.followRequestSent = sent
this.updated = updated
})
},
unfollowUser () {
this.followRequestInProgress = true
requestUnfollow(this.user, this.$store).then(({ updated }) => {
this.followRequestInProgress = false
this.updated = updated
})
}
}
}
export default UserCard

View file

@ -1,159 +0,0 @@
<template>
<div class="card">
<router-link :to="userProfileLink(user)">
<UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
</router-link>
<div class="user-card-main-content">
<div class="usercard" v-if="userExpanded">
<user-card-content :user="user" :switcher="false"></user-card-content>
</div>
<div class="name-and-screen-name" v-if="!userExpanded">
<div :title="user.name" class="user-name">
<span v-if="user.name_html" v-html="user.name_html"></span>
<span v-else>{{ user.name }}</span>
</div>
<div class="user-link-action">
<router-link class='user-screen-name' :to="userProfileLink(user)">
@{{user.screen_name}}
</router-link>
</div>
</div>
<div class="follow-box" v-if="!userExpanded">
<span class="faint" v-if="!noFollowsYou && user.follows_you">
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span>
<button
v-if="showFollow"
class="btn btn-default"
@click="followUser"
:disabled="followRequestInProgress"
:title="followRequestSent ? $t('user_card.follow_again') : ''"
>
<template v-if="followRequestInProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else-if="followRequestSent">
{{ $t('user_card.follow_sent') }}
</template>
<template v-else>
{{ $t('user_card.follow') }}
</template>
</button>
<button v-if="following" class="btn btn-default pressed" @click="unfollowUser" :disabled="followRequestInProgress">
<template v-if="followRequestInProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else>
{{ $t('user_card.follow_unfollow') }}
</template>
</button>
</div>
<div class="approval" v-if="showApproval">
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
<button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button>
</div>
</div>
</div>
</template>
<script src="./user_card.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.user-card-main-content {
display: flex;
flex-direction: column;
flex: 1 1 100%;
margin-left: 0.7em;
min-width: 0;
}
.name-and-screen-name {
text-align: left;
width: 100%;
.user-name {
img {
object-fit: contain;
height: 16px;
width: 16px;
vertical-align: middle;
}
}
.user-link-action {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
}
.card {
display: flex;
flex: 1 0;
padding-top: 0.6em;
padding-right: 1em;
padding-bottom: 0.6em;
padding-left: 1em;
border-bottom: 1px solid;
margin: 0;
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
.avatar {
padding: 0;
}
.follow-box {
text-align: center;
flex-shrink: 0;
display: flex;
flex-direction: row;
justify-content: space-between;
flex-wrap: wrap;
line-height: 1.5em;
.btn {
margin-top: 0.5em;
margin-left: auto;
width: 10em;
}
}
}
.usercard {
width: fill-available;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-width: 1px;
overflow: hidden;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
p {
margin-bottom: 0;
}
}
.approval {
display: flex;
flex-direction: row;
flex-wrap: wrap;
button {
margin-top: 0.5em;
margin-right: 0.5em;
flex: 1 1;
max-width: 12em;
min-width: 8em;
}
}
</style>

View file

@ -222,6 +222,13 @@
overflow: hidden; overflow: hidden;
flex: 1 1 auto; flex: 1 1 auto;
margin-right: 1em; margin-right: 1em;
img {
object-fit: contain;
height: 16px;
width: 16px;
vertical-align: middle;
}
} }
.user-screen-name { .user-screen-name {
@ -386,4 +393,24 @@
} }
} }
.usercard {
width: fill-available;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-width: 1px;
overflow: hidden;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
p {
margin-bottom: 0;
}
}
</style> </style>

View file

@ -1,9 +1,39 @@
import { compose } from 'vue-compose'
import get from 'lodash/get'
import UserCardContent from '../user_card_content/user_card_content.vue' import UserCardContent from '../user_card_content/user_card_content.vue'
import UserCard from '../user_card/user_card.vue' import FollowCard from '../follow_card/follow_card.vue'
import Timeline from '../timeline/timeline.vue' import Timeline from '../timeline/timeline.vue'
import FollowList from '../follow_list/follow_list.vue' import withLoadMore from '../../hocs/with_load_more/with_load_more'
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', []),
destory: (props, $store) => $store.dispatch('clearFollowers', props.userId),
childPropName: 'entries',
additionalPropNames: ['userId']
}),
withList({ getEntryProps: user => ({ user }) })
)(FollowCard)
const FriendList = compose(
withLoadMore({
fetch: (props, $store) => $store.dispatch('addFriends', props.userId),
select: (props, $store) => get($store.getters.userById(props.userId), 'friends', []),
destory: (props, $store) => $store.dispatch('clearFriends', props.userId),
childPropName: 'entries',
additionalPropNames: ['userId']
}),
withList({ getEntryProps: user => ({ user }) })
)(FollowCard)
const UserProfile = { const UserProfile = {
data () {
return {
error: false
}
},
created () { created () {
this.$store.commit('clearTimeline', { timeline: 'user' }) this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.commit('clearTimeline', { timeline: 'favorites' }) this.$store.commit('clearTimeline', { timeline: 'favorites' })
@ -13,6 +43,16 @@ const UserProfile = {
this.startFetchFavorites() this.startFetchFavorites()
if (!this.user.id) { if (!this.user.id) {
this.$store.dispatch('fetchUser', this.fetchBy) 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')
}
})
} }
}, },
destroyed () { destroyed () {
@ -105,9 +145,9 @@ const UserProfile = {
}, },
components: { components: {
UserCardContent, UserCardContent,
UserCard,
Timeline, Timeline,
FollowList FollowerList,
FriendList
} }
} }

View file

@ -18,16 +18,10 @@
:user-id="fetchBy" :user-id="fetchBy"
/> />
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count"> <div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
<FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" /> <FriendList :userId="userId" />
<div class="userlist-placeholder" v-else>
<i class="icon-spin3 animate-spin"></i>
</div>
</div> </div>
<div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count"> <div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
<FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" /> <FollowerList :userId="userId" :entryProps="{noFollowsYou: isUs}" />
<div class="userlist-placeholder" v-else>
<i class="icon-spin3 animate-spin"></i>
</div>
</div> </div>
<Timeline <Timeline
:label="$t('user_card.media')" :label="$t('user_card.media')"
@ -55,7 +49,8 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<i class="icon-spin3 animate-spin"></i> <span v-if="error">{{ error }}</span>
<i class="icon-spin3 animate-spin" v-else></i>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,8 +1,8 @@
import UserCard from '../user_card/user_card.vue' import FollowCard from '../follow_card/follow_card.vue'
import userSearchApi from '../../services/new_api/user_search.js' import userSearchApi from '../../services/new_api/user_search.js'
const userSearch = { const userSearch = {
components: { components: {
UserCard FollowCard
}, },
props: [ props: [
'query' 'query'

View file

@ -13,7 +13,7 @@
<i class="icon-spin3 animate-spin"/> <i class="icon-spin3 animate-spin"/>
</div> </div>
<div v-else class="panel-body"> <div v-else class="panel-body">
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card> <FollowCard v-for="user in users" :key="user.id" :user="user"/>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,7 +1,6 @@
import { compose } from 'vue-compose' import { compose } from 'vue-compose'
import unescape from 'lodash/unescape' import unescape from 'lodash/unescape'
import get from 'lodash/get' import get from 'lodash/get'
import TabSwitcher from '../tab_switcher/tab_switcher.js' import TabSwitcher from '../tab_switcher/tab_switcher.js'
import ImageCropper from '../image_cropper/image_cropper.vue' import ImageCropper from '../image_cropper/image_cropper.vue'
import StyleSwitcher from '../style_switcher/style_switcher.vue' import StyleSwitcher from '../style_switcher/style_switcher.vue'
@ -62,6 +61,9 @@ const UserSettings = {
activeTab: 'profile' activeTab: 'profile'
} }
}, },
created () {
this.$store.dispatch('fetchTokens')
},
components: { components: {
StyleSwitcher, StyleSwitcher,
TabSwitcher, TabSwitcher,
@ -89,6 +91,15 @@ const UserSettings = {
}, },
currentSaveStateNotice () { currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice return this.$store.state.interface.settings.currentSaveStateNotice
},
oauthTokens () {
return this.$store.state.oauthTokens.tokens.map(oauthToken => {
return {
id: oauthToken.id,
appName: oauthToken.app_name,
validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
}
})
} }
}, },
methods: { methods: {
@ -308,6 +319,11 @@ const UserSettings = {
logout () { logout () {
this.$store.dispatch('logout') this.$store.dispatch('logout')
this.$router.replace('/') this.$router.replace('/')
},
revokeToken (id) {
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
this.$store.dispatch('revokeToken', id)
}
} }
} }
} }

View file

@ -121,6 +121,30 @@
<p v-if="changePasswordError">{{changePasswordError}}</p> <p v-if="changePasswordError">{{changePasswordError}}</p>
</div> </div>
<div class="setting-item">
<h2>{{$t('settings.oauth_tokens')}}</h2>
<table class="oauth-tokens">
<thead>
<tr>
<th>{{$t('settings.app_name')}}</th>
<th>{{$t('settings.valid_until')}}</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-for="oauthToken in oauthTokens" :key="oauthToken.id">
<td>{{oauthToken.appName}}</td>
<td>{{oauthToken.validUntil}}</td>
<td class="actions">
<button class="btn btn-default" @click="revokeToken(oauthToken.id)">
{{$t('settings.revoke_token')}}
</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class="setting-item"> <div class="setting-item">
<h2>{{$t('settings.delete_account')}}</h2> <h2>{{$t('settings.delete_account')}}</h2>
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p> <p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
@ -213,5 +237,17 @@
border-radius: $fallback--avatarRadius; border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius); border-radius: var(--avatarRadius, $fallback--avatarRadius);
} }
.oauth-tokens {
width: 100%;
th {
text-align: left;
}
.actions {
text-align: right;
}
}
} }
</style> </style>

View file

@ -1,9 +1,9 @@
import apiService from '../../services/api/api.service.js' import apiService from '../../services/api/api.service.js'
import UserCard from '../user_card/user_card.vue' import FollowCard from '../follow_card/follow_card.vue'
const WhoToFollow = { const WhoToFollow = {
components: { components: {
UserCard FollowCard
}, },
data () { data () {
return { return {

View file

@ -4,7 +4,7 @@
{{$t('who_to_follow.who_to_follow')}} {{$t('who_to_follow.who_to_follow')}}
</div> </div>
<div class="panel-body"> <div class="panel-body">
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card> <FollowCard v-for="user in users" :key="user.id" :user="user"/>
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,15 +1,17 @@
import Vue from 'vue' import Vue from 'vue'
import filter from 'lodash/filter'
import isEmpty from 'lodash/isEmpty' import isEmpty from 'lodash/isEmpty'
import { getComponentProps } from '../../services/component_utils/component_utils'
import './with_load_more.scss' import './with_load_more.scss'
const withLoadMore = ({ const withLoadMore = ({
fetch, // function to fetch entries and return a promise fetch, // function to fetch entries and return a promise
select, // function to select data from store select, // function to select data from store
childPropName = 'entries' // name of the prop to be passed into the wrapped component destroy, // function called at "destroyed" lifecycle
childPropName = 'entries', // name of the prop to be passed into the wrapped component
additionalPropNames = [] // additional prop name list of the wrapper component
}) => (WrappedComponent) => { }) => (WrappedComponent) => {
const originalProps = WrappedComponent.props || [] const originalProps = Object.keys(getComponentProps(WrappedComponent))
const props = filter(originalProps, v => v !== 'entries') const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames)
return Vue.component('withLoadMore', { return Vue.component('withLoadMore', {
render (createElement) { render (createElement) {
@ -56,6 +58,7 @@ const withLoadMore = ({
}, },
destroyed () { destroyed () {
window.removeEventListener('scroll', this.scrollLoad) window.removeEventListener('scroll', this.scrollLoad)
destroy && destroy(this.$props, this.$store)
}, },
methods: { methods: {
fetchEntries () { fetchEntries () {

View file

@ -1,16 +1,16 @@
import Vue from 'vue' import Vue from 'vue'
import reject from 'lodash/reject'
import isEmpty from 'lodash/isEmpty' import isEmpty from 'lodash/isEmpty'
import omit from 'lodash/omit' import { getComponentProps } from '../../services/component_utils/component_utils'
import './with_subscription.scss' import './with_subscription.scss'
const withSubscription = ({ const withSubscription = ({
fetch, // function to fetch entries and return a promise fetch, // function to fetch entries and return a promise
select, // function to select data from store select, // function to select data from store
childPropName = 'content' // name of the prop to be passed into the wrapped component childPropName = 'content', // name of the prop to be passed into the wrapped component
additionalPropNames = [] // additional prop name list of the wrapper component
}) => (WrappedComponent) => { }) => (WrappedComponent) => {
const originalProps = WrappedComponent.props || [] const originalProps = Object.keys(getComponentProps(WrappedComponent))
const props = reject(originalProps, v => v === 'content') const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames)
return Vue.component('withSubscription', { return Vue.component('withSubscription', {
props: [ props: [
@ -21,7 +21,7 @@ const withSubscription = ({
if (!this.error && !this.loading) { if (!this.error && !this.loading) {
const props = { const props = {
props: { props: {
...omit(this.$props, 'refresh'), ...this.$props,
[childPropName]: this.fetchedData [childPropName]: this.fetchedData
}, },
on: this.$listeners, on: this.$listeners,

View file

@ -134,6 +134,11 @@
"notification_visibility_mentions": "الإشارات", "notification_visibility_mentions": "الإشارات",
"notification_visibility_repeats": "", "notification_visibility_repeats": "",
"nsfw_clickthrough": "", "nsfw_clickthrough": "",
"oauth_tokens": "رموز OAuth",
"token": "رمز",
"refresh_token": "رمز التحديث",
"valid_until": "صالح حتى",
"revoke_token": "سحب",
"panelRadius": "", "panelRadius": "",
"pause_on_unfocused": "", "pause_on_unfocused": "",
"presets": "النماذج", "presets": "النماذج",

View file

@ -132,6 +132,11 @@
"notification_visibility_repeats": "Republica una entrada meva", "notification_visibility_repeats": "Republica una entrada meva",
"no_rich_text_description": "Neteja el formatat de text de totes les entrades", "no_rich_text_description": "Neteja el formatat de text de totes les entrades",
"nsfw_clickthrough": "Amaga el contingut NSFW darrer d'una imatge clicable", "nsfw_clickthrough": "Amaga el contingut NSFW darrer d'una imatge clicable",
"oauth_tokens": "Llistats OAuth",
"token": "Token",
"refresh_token": "Actualitza el token",
"valid_until": "Vàlid fins",
"revoke_token": "Revocar",
"panelRadius": "Panells", "panelRadius": "Panells",
"pause_on_unfocused": "Pausa la reproducció en continu quan la pestanya perdi el focus", "pause_on_unfocused": "Pausa la reproducció en continu quan la pestanya perdi el focus",
"presets": "Temes", "presets": "Temes",

View file

@ -159,6 +159,11 @@
"hide_follows_description": "Zeige nicht, wem ich folge", "hide_follows_description": "Zeige nicht, wem ich folge",
"hide_followers_description": "Zeige nicht, wer mir folgt", "hide_followers_description": "Zeige nicht, wer mir folgt",
"nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind", "nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind",
"oauth_tokens": "OAuth-Token",
"token": "Zeichen",
"refresh_token": "Token aktualisieren",
"valid_until": "Gültig bis",
"revoke_token": "Widerrufen",
"panelRadius": "Panel", "panelRadius": "Panel",
"pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist", "pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist",
"presets": "Voreinstellungen", "presets": "Voreinstellungen",

View file

@ -106,6 +106,7 @@
} }
}, },
"settings": { "settings": {
"app_name": "App name",
"attachmentRadius": "Attachments", "attachmentRadius": "Attachments",
"attachments": "Attachments", "attachments": "Attachments",
"autoload": "Enable automatic loading when scrolled to the bottom", "autoload": "Enable automatic loading when scrolled to the bottom",
@ -149,6 +150,7 @@
"general": "General", "general": "General",
"hide_attachments_in_convo": "Hide attachments in conversations", "hide_attachments_in_convo": "Hide attachments in conversations",
"hide_attachments_in_tl": "Hide attachments in timeline", "hide_attachments_in_tl": "Hide attachments in timeline",
"max_thumbnails": "Maximum amount of thumbnails per post",
"hide_isp": "Hide instance-specific panel", "hide_isp": "Hide instance-specific panel",
"preload_images": "Preload images", "preload_images": "Preload images",
"use_one_click_nsfw": "Open NSFW attachments with just one click", "use_one_click_nsfw": "Open NSFW attachments with just one click",
@ -188,6 +190,11 @@
"show_admin_badge": "Show Admin badge in my profile", "show_admin_badge": "Show Admin badge in my profile",
"show_moderator_badge": "Show Moderator badge in my profile", "show_moderator_badge": "Show Moderator badge in my profile",
"nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding", "nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
"oauth_tokens": "OAuth tokens",
"token": "Token",
"refresh_token": "Refresh Token",
"valid_until": "Valid Until",
"revoke_token": "Revoke",
"panelRadius": "Panels", "panelRadius": "Panels",
"pause_on_unfocused": "Pause streaming when tab is not focused", "pause_on_unfocused": "Pause streaming when tab is not focused",
"presets": "Presets", "presets": "Presets",
@ -383,7 +390,9 @@
"mute_progress": "Muting..." "mute_progress": "Muting..."
}, },
"user_profile": { "user_profile": {
"timeline_title": "User Timeline" "timeline_title": "User Timeline",
"profile_does_not_exist": "Sorry, this profile does not exist.",
"profile_loading_error": "Sorry, there was an error loading this profile."
}, },
"who_to_follow": { "who_to_follow": {
"more": "More", "more": "More",

View file

@ -171,6 +171,11 @@
"show_admin_badge": "Mostrar la placa de administrador en mi perfil", "show_admin_badge": "Mostrar la placa de administrador en mi perfil",
"show_moderator_badge": "Mostrar la placa de moderador en mi perfil", "show_moderator_badge": "Mostrar la placa de moderador en mi perfil",
"nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW", "nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
"oauth_tokens": "Tokens de OAuth",
"token": "Token",
"refresh_token": "Actualizar el token",
"valid_until": "Válido hasta",
"revoke_token": "Revocar",
"panelRadius": "Paneles", "panelRadius": "Paneles",
"pause_on_unfocused": "Parar la transmisión cuando no estés en foco.", "pause_on_unfocused": "Parar la transmisión cuando no estés en foco.",
"presets": "Por defecto", "presets": "Por defecto",

View file

@ -133,6 +133,7 @@
"general": "Yleinen", "general": "Yleinen",
"hide_attachments_in_convo": "Piilota liitteet keskusteluissa", "hide_attachments_in_convo": "Piilota liitteet keskusteluissa",
"hide_attachments_in_tl": "Piilota liitteet aikajanalla", "hide_attachments_in_tl": "Piilota liitteet aikajanalla",
"max_thumbnails": "Suurin sallittu määrä liitteitä esikatselussa",
"hide_isp": "Piilota palvelimenkohtainen ruutu", "hide_isp": "Piilota palvelimenkohtainen ruutu",
"preload_images": "Esilataa kuvat", "preload_images": "Esilataa kuvat",
"use_one_click_nsfw": "Avaa NSFW-liitteet yhdellä painalluksella", "use_one_click_nsfw": "Avaa NSFW-liitteet yhdellä painalluksella",
@ -165,6 +166,11 @@
"no_rich_text_description": "Älä näytä tekstin muotoilua.", "no_rich_text_description": "Älä näytä tekstin muotoilua.",
"hide_network_description": "Älä näytä seurauksiani tai seuraajiani", "hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
"nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse", "nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse",
"oauth_tokens": "OAuth-merkit",
"token": "Token",
"refresh_token": "Päivitä token",
"valid_until": "Voimassa asti",
"revoke_token": "Peruuttaa",
"panelRadius": "Ruudut", "panelRadius": "Ruudut",
"pause_on_unfocused": "Pysäytä automaattinen viestien näyttö välilehden ollessa pois fokuksesta", "pause_on_unfocused": "Pysäytä automaattinen viestien näyttö välilehden ollessa pois fokuksesta",
"presets": "Valmiit teemat", "presets": "Valmiit teemat",

View file

@ -137,6 +137,11 @@
"notification_visibility_mentions": "Mentionnés", "notification_visibility_mentions": "Mentionnés",
"notification_visibility_repeats": "Partages", "notification_visibility_repeats": "Partages",
"nsfw_clickthrough": "Masquer les images marquées comme contenu adulte ou sensible", "nsfw_clickthrough": "Masquer les images marquées comme contenu adulte ou sensible",
"oauth_tokens": "Jetons OAuth",
"token": "Jeton",
"refresh_token": "Refresh Token",
"valid_until": "Valable jusque",
"revoke_token": "Révoquer",
"panelRadius": "Fenêtres", "panelRadius": "Fenêtres",
"pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas centré", "pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas centré",
"presets": "Thèmes prédéfinis", "presets": "Thèmes prédéfinis",

View file

@ -134,6 +134,11 @@
"notification_visibility_repeats": "Atphostáil", "notification_visibility_repeats": "Atphostáil",
"no_rich_text_description": "Bain formáidiú téacs saibhir ó gach post", "no_rich_text_description": "Bain formáidiú téacs saibhir ó gach post",
"nsfw_clickthrough": "Cumasaigh an ceangaltán NSFW cliceáil ar an gcnaipe", "nsfw_clickthrough": "Cumasaigh an ceangaltán NSFW cliceáil ar an gcnaipe",
"oauth_tokens": "Tocanna OAuth",
"token": "Token",
"refresh_token": "Athnuachan Comórtas",
"valid_until": "Bailí Go dtí",
"revoke_token": "Athghairm",
"panelRadius": "Painéil", "panelRadius": "Painéil",
"pause_on_unfocused": "Sruthú ar sos nuair a bhíonn an fócas caillte", "pause_on_unfocused": "Sruthú ar sos nuair a bhíonn an fócas caillte",
"presets": "Réamhshocruithe", "presets": "Réamhshocruithe",

View file

@ -129,6 +129,11 @@
"notification_visibility_mentions": "אזכורים", "notification_visibility_mentions": "אזכורים",
"notification_visibility_repeats": "חזרות", "notification_visibility_repeats": "חזרות",
"nsfw_clickthrough": "החל החבאת צירופים לא בטוחים לצפיה בעת עבודה בעזרת לחיצת עכבר", "nsfw_clickthrough": "החל החבאת צירופים לא בטוחים לצפיה בעת עבודה בעזרת לחיצת עכבר",
"oauth_tokens": "אסימוני OAuth",
"token": "אסימון",
"refresh_token": "רענון האסימון",
"valid_until": "בתוקף עד",
"revoke_token": "בטל",
"panelRadius": "פאנלים", "panelRadius": "פאנלים",
"pause_on_unfocused": "השהה זרימת הודעות כשהחלון לא בפוקוס", "pause_on_unfocused": "השהה זרימת הודעות כשהחלון לא בפוקוס",
"presets": "ערכים קבועים מראש", "presets": "ערכים קבועים מראש",

View file

@ -93,6 +93,11 @@
"notification_visibility_mentions": "Menzioni", "notification_visibility_mentions": "Menzioni",
"notification_visibility_repeats": "Condivisioni", "notification_visibility_repeats": "Condivisioni",
"no_rich_text_description": "Togli la formattazione del testo da tutti i post", "no_rich_text_description": "Togli la formattazione del testo da tutti i post",
"oauth_tokens": "Token OAuth",
"token": "Token",
"refresh_token": "Aggiorna token",
"valid_until": "Valido fino a",
"revoke_token": "Revocare",
"panelRadius": "Pannelli", "panelRadius": "Pannelli",
"pause_on_unfocused": "Metti in pausa l'aggiornamento continuo quando la scheda non è in primo piano", "pause_on_unfocused": "Metti in pausa l'aggiornamento continuo quando la scheda non è in primo piano",
"presets": "Valori predefiniti", "presets": "Valori predefiniti",

View file

@ -171,6 +171,11 @@
"show_admin_badge": "アドミンのしるしをみる", "show_admin_badge": "アドミンのしるしをみる",
"show_moderator_badge": "モデレーターのしるしをみる", "show_moderator_badge": "モデレーターのしるしをみる",
"nsfw_clickthrough": "NSFWなファイルをかくす", "nsfw_clickthrough": "NSFWなファイルをかくす",
"oauth_tokens": "OAuthトークン",
"token": "トークン",
"refresh_token": "トークンを更新",
"valid_until": "まで有効",
"revoke_token": "取り消す",
"panelRadius": "パネル", "panelRadius": "パネル",
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる", "pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
"presets": "プリセット", "presets": "プリセット",

View file

@ -159,6 +159,11 @@
"hide_follows_description": "내가 팔로우하는 사람을 표시하지 않음", "hide_follows_description": "내가 팔로우하는 사람을 표시하지 않음",
"hide_followers_description": "나를 따르는 사람을 보여주지 마라.", "hide_followers_description": "나를 따르는 사람을 보여주지 마라.",
"nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화", "nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화",
"oauth_tokens": "OAuth 토큰",
"token": "토큰",
"refresh_token": "토큰 새로 고침",
"valid_until": "까지 유효하다",
"revoke_token": "취소",
"panelRadius": "패널", "panelRadius": "패널",
"pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기", "pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기",
"presets": "프리셋", "presets": "프리셋",

View file

@ -132,6 +132,11 @@
"notification_visibility_repeats": "Gjentakelser", "notification_visibility_repeats": "Gjentakelser",
"no_rich_text_description": "Fjern all formatering fra statuser", "no_rich_text_description": "Fjern all formatering fra statuser",
"nsfw_clickthrough": "Krev trykk for å vise statuser som kan være upassende", "nsfw_clickthrough": "Krev trykk for å vise statuser som kan være upassende",
"oauth_tokens": "OAuth Tokens",
"token": "Pollett",
"refresh_token": "Refresh Token",
"valid_until": "Gyldig til",
"revoke_token": "Tilbakekall",
"panelRadius": "Panel", "panelRadius": "Panel",
"pause_on_unfocused": "Stopp henting av poster når vinduet ikke er i fokus", "pause_on_unfocused": "Stopp henting av poster når vinduet ikke er i fokus",
"presets": "Forhåndsdefinerte tema", "presets": "Forhåndsdefinerte tema",

View file

@ -159,6 +159,11 @@
"no_rich_text_description": "Strip rich text formattering van alle posts", "no_rich_text_description": "Strip rich text formattering van alle posts",
"hide_network_description": "Toon niet wie mij volgt en wie ik volg.", "hide_network_description": "Toon niet wie mij volgt en wie ik volg.",
"nsfw_clickthrough": "Schakel doorklikbaar verbergen van NSFW bijlages in", "nsfw_clickthrough": "Schakel doorklikbaar verbergen van NSFW bijlages in",
"oauth_tokens": "OAuth-tokens",
"token": "Token",
"refresh_token": "Token vernieuwen",
"valid_until": "Geldig tot",
"revoke_token": "Intrekken",
"panelRadius": "Panelen", "panelRadius": "Panelen",
"pause_on_unfocused": "Pauzeer streamen wanneer de tab niet gefocused is", "pause_on_unfocused": "Pauzeer streamen wanneer de tab niet gefocused is",
"presets": "Presets", "presets": "Presets",

View file

@ -142,6 +142,7 @@
"notification_visibility_mentions": "Mencions", "notification_visibility_mentions": "Mencions",
"notification_visibility_repeats": "Repeticions", "notification_visibility_repeats": "Repeticions",
"no_rich_text_description": "Netejar lo format tèxte de totas las publicacions", "no_rich_text_description": "Netejar lo format tèxte de totas las publicacions",
"oauth_tokens": "Llistats OAuth",
"pause_on_unfocused": "Pausar la difusion quand longlet es pas seleccionat", "pause_on_unfocused": "Pausar la difusion quand longlet es pas seleccionat",
"profile_tab": "Perfil", "profile_tab": "Perfil",
"replies_in_timeline": "Responsas del flux", "replies_in_timeline": "Responsas del flux",

View file

@ -86,6 +86,11 @@
"name_bio": "Imię i bio", "name_bio": "Imię i bio",
"new_password": "Nowe hasło", "new_password": "Nowe hasło",
"nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)", "nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)",
"oauth_tokens": "Tokeny OAuth",
"token": "Token",
"refresh_token": "Odśwież token",
"valid_until": "Ważne do",
"revoke_token": "Odwołać",
"panelRadius": "Panele", "panelRadius": "Panele",
"presets": "Gotowe motywy", "presets": "Gotowe motywy",
"profile_background": "Tło profilu", "profile_background": "Tło profilu",

View file

@ -132,6 +132,11 @@
"show_admin_badge": "Показывать значок администратора в моем профиле", "show_admin_badge": "Показывать значок администратора в моем профиле",
"show_moderator_badge": "Показывать значок модератора в моем профиле", "show_moderator_badge": "Показывать значок модератора в моем профиле",
"nsfw_clickthrough": "Включить скрытие NSFW вложений", "nsfw_clickthrough": "Включить скрытие NSFW вложений",
"oauth_tokens": "OAuth токены",
"token": "Токен",
"refresh_token": "Рефреш токен",
"valid_until": "Годен до",
"revoke_token": "Удалить",
"panelRadius": "Панели", "panelRadius": "Панели",
"pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе", "pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",
"presets": "Пресеты", "presets": "Пресеты",

View file

@ -134,6 +134,11 @@
"notification_visibility_repeats": "转发", "notification_visibility_repeats": "转发",
"no_rich_text_description": "不显示富文本格式", "no_rich_text_description": "不显示富文本格式",
"nsfw_clickthrough": "将不和谐附件隐藏,点击才能打开", "nsfw_clickthrough": "将不和谐附件隐藏,点击才能打开",
"oauth_tokens": "OAuth令牌",
"token": "代币",
"refresh_token": "刷新令牌",
"valid_until": "有效期至",
"revoke_token": "撤消",
"panelRadius": "面板", "panelRadius": "面板",
"pause_on_unfocused": "在离开页面时暂停时间线推送", "pause_on_unfocused": "在离开页面时暂停时间线推送",
"presets": "预置", "presets": "预置",

View file

@ -11,6 +11,7 @@ import configModule from './modules/config.js'
import chatModule from './modules/chat.js' import chatModule from './modules/chat.js'
import oauthModule from './modules/oauth.js' import oauthModule from './modules/oauth.js'
import mediaViewerModule from './modules/media_viewer.js' import mediaViewerModule from './modules/media_viewer.js'
import oauthTokensModule from './modules/oauth_tokens.js'
import VueTimeago from 'vue-timeago' import VueTimeago from 'vue-timeago'
import VueI18n from 'vue-i18n' import VueI18n from 'vue-i18n'
@ -64,7 +65,8 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
config: configModule, config: configModule,
chat: chatModule, chat: chatModule,
oauth: oauthModule, oauth: oauthModule,
mediaViewer: mediaViewerModule mediaViewer: mediaViewerModule,
oauthTokens: oauthTokensModule
}, },
plugins: [persistedState, pushNotifications], plugins: [persistedState, pushNotifications],
strict: false // Socket modifies itself, let's ignore this for now. strict: false // Socket modifies itself, let's ignore this for now.

View file

@ -8,6 +8,7 @@ const defaultState = {
collapseMessageWithSubject: undefined, // instance default collapseMessageWithSubject: undefined, // instance default
hideAttachments: false, hideAttachments: false,
hideAttachmentsInConv: false, hideAttachmentsInConv: false,
maxThumbnails: 16,
hideNsfw: true, hideNsfw: true,
preloadImage: true, preloadImage: true,
loopVideo: true, loopVideo: true,

View file

@ -0,0 +1,26 @@
const oauthTokens = {
state: {
tokens: []
},
actions: {
fetchTokens ({rootState, commit}) {
rootState.api.backendInteractor.fetchOAuthTokens().then((tokens) => {
commit('swapTokens', tokens)
})
},
revokeToken ({rootState, commit, state}, id) {
rootState.api.backendInteractor.revokeOAuthToken(id).then((response) => {
if (response.status === 201) {
commit('swapTokens', state.tokens.filter(token => token.id !== id))
}
})
}
},
mutations: {
swapTokens (state, tokens) {
state.tokens = tokens
}
}
}
export default oauthTokens

View file

@ -72,14 +72,20 @@ export const mutations = {
}, },
// Because frontend doesn't have a reason to keep these stuff in memory // Because frontend doesn't have a reason to keep these stuff in memory
// outside of viewing someones user profile. // outside of viewing someones user profile.
clearFriendsAndFollowers (state, userKey) { clearFriends (state, userId) {
const user = state.usersObject[userKey] const user = state.usersObject[userId]
if (!user) { if (!user) {
return return
} }
user.friends = [] user.friends = []
user.followers = []
user.friendsPage = 0 user.friendsPage = 0
},
clearFollowers (state, userId) {
const user = state.usersObject[userId]
if (!user) {
return
}
user.followers = []
user.followersPage = 0 user.followersPage = 0
}, },
addNewUsers (state, users) { addNewUsers (state, users) {
@ -140,7 +146,7 @@ const users = {
getters, getters,
actions: { actions: {
fetchUser (store, id) { fetchUser (store, id) {
store.rootState.api.backendInteractor.fetchUser({ id }) return store.rootState.api.backendInteractor.fetchUser({ id })
.then((user) => store.commit('addNewUsers', [user])) .then((user) => store.commit('addNewUsers', [user]))
}, },
fetchBlocks (store) { fetchBlocks (store) {
@ -189,20 +195,19 @@ const users = {
}) })
}, },
addFollowers ({ rootState, commit }, fetchBy) { addFollowers ({ rootState, commit }, fetchBy) {
return new Promise((resolve, reject) => { const user = rootState.users.usersObject[fetchBy]
const user = rootState.users.usersObject[fetchBy] const page = user.followersPage || 1
const page = user.followersPage || 1 return rootState.api.backendInteractor.fetchFollowers({ id: user.id, page })
rootState.api.backendInteractor.fetchFollowers({ id: user.id, page }) .then((followers) => {
.then((followers) => { commit('addFollowers', { id: user.id, followers, page })
commit('addFollowers', { id: user.id, followers, page }) return followers
resolve(followers) })
}).catch(() => {
reject()
})
})
}, },
clearFriendsAndFollowers ({ commit }, userKey) { clearFriends ({ commit }, userId) {
commit('clearFriendsAndFollowers', userKey) commit('clearFriends', userId)
},
clearFollowers ({ commit }, userId) {
commit('clearFollowers', userId)
}, },
registerPushNotifications (store) { registerPushNotifications (store) {
const token = store.state.currentUser.credentials const token = store.state.currentUser.credentials

View file

@ -47,6 +47,7 @@ const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
import { each, map } from 'lodash' import { each, map } from 'lodash'
import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js' import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
import 'whatwg-fetch' import 'whatwg-fetch'
import { StatusCodeError } from '../errors/errors'
const oldfetch = window.fetch const oldfetch = window.fetch
@ -244,7 +245,15 @@ const denyUser = ({id, credentials}) => {
const fetchUser = ({id, credentials}) => { const fetchUser = ({id, credentials}) => {
let url = `${USER_URL}?user_id=${id}` let url = `${USER_URL}?user_id=${id}`
return fetch(url, { headers: authHeaders(credentials) }) return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json()) .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)
}))
})
.then((data) => parseUser(data)) .then((data) => parseUser(data))
} }
@ -531,6 +540,23 @@ const fetchBlocks = ({page, credentials}) => {
}) })
} }
const fetchOAuthTokens = ({credentials}) => {
const url = '/api/oauth_tokens.json'
return fetch(url, {
headers: authHeaders(credentials)
}).then((data) => data.json())
}
const revokeOAuthToken = ({id, credentials}) => {
const url = `/api/oauth_tokens/${id}`
return fetch(url, {
headers: authHeaders(credentials),
method: 'DELETE'
})
}
const suggestions = ({credentials}) => { const suggestions = ({credentials}) => {
return fetch(SUGGESTIONS_URL, { return fetch(SUGGESTIONS_URL, {
headers: authHeaders(credentials) headers: authHeaders(credentials)
@ -573,6 +599,8 @@ const apiService = {
setUserMute, setUserMute,
fetchMutes, fetchMutes,
fetchBlocks, fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
register, register,
getCaptcha, getCaptcha,
updateAvatar, updateAvatar,

View file

@ -65,6 +65,8 @@ const backendInteractorService = (credentials) => {
const fetchMutes = () => apiService.fetchMutes({credentials}) const fetchMutes = () => apiService.fetchMutes({credentials})
const fetchBlocks = (params) => apiService.fetchBlocks({credentials, ...params}) const fetchBlocks = (params) => apiService.fetchBlocks({credentials, ...params})
const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials}) const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials})
const fetchOAuthTokens = () => apiService.fetchOAuthTokens({credentials})
const revokeOAuthToken = (id) => apiService.revokeOAuthToken({id, credentials})
const getCaptcha = () => apiService.getCaptcha() const getCaptcha = () => apiService.getCaptcha()
const register = (params) => apiService.register(params) const register = (params) => apiService.register(params)
@ -96,6 +98,8 @@ const backendInteractorService = (credentials) => {
setUserMute, setUserMute,
fetchMutes, fetchMutes,
fetchBlocks, fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
register, register,
getCaptcha, getCaptcha,
updateAvatar, updateAvatar,

View file

@ -0,0 +1,10 @@
import isFunction from 'lodash/isFunction'
const getComponentOptions = (Component) => (isFunction(Component)) ? Component.options : Component
const getComponentProps = (Component) => getComponentOptions(Component).props
export {
getComponentOptions,
getComponentProps
}

View file

@ -0,0 +1,14 @@
export function StatusCodeError (statusCode, body, options, response) {
this.name = 'StatusCodeError'
this.statusCode = statusCode
this.message = statusCode + ' - ' + (JSON && JSON.stringify ? JSON.stringify(body) : body)
this.error = body // legacy attribute
this.options = options
this.response = response
if (Error.captureStackTrace) { // required for non-V8 environments
Error.captureStackTrace(this)
}
}
StatusCodeError.prototype = Object.create(Error.prototype)
StatusCodeError.prototype.constructor = StatusCodeError

View file

@ -0,0 +1,21 @@
import apiService from '../api/api.service.js'
const fetchAndUpdate = ({ store, credentials }) => {
return apiService.fetchFollowRequests({ credentials })
.then((requests) => {
store.commit('setFollowRequests', requests)
}, () => {})
.catch(() => {})
}
const startFetching = ({credentials, store}) => {
fetchAndUpdate({ credentials, store })
const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store })
return setInterval(boundFetchAndUpdate, 10000)
}
const followRequestFetcher = {
startFetching
}
export default followRequestFetcher