aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/basic_user_card/basic_user_card.vue47
-rw-r--r--src/components/block_card/block_card.vue16
-rw-r--r--src/components/follow_card/follow_card.js45
-rw-r--r--src/components/follow_card/follow_card.vue53
-rw-r--r--src/components/follow_list/follow_list.js68
-rw-r--r--src/components/follow_list/follow_list.vue33
-rw-r--r--src/components/follow_request_card/follow_request_card.js20
-rw-r--r--src/components/follow_request_card/follow_request_card.vue29
-rw-r--r--src/components/follow_requests/follow_requests.js13
-rw-r--r--src/components/follow_requests/follow_requests.vue2
-rw-r--r--src/components/nav_panel/nav_panel.js13
-rw-r--r--src/components/nav_panel/nav_panel.vue4
-rw-r--r--src/components/settings/settings.js5
-rw-r--r--src/components/settings/settings.vue10
-rw-r--r--src/components/side_drawer/side_drawer.js3
-rw-r--r--src/components/side_drawer/side_drawer.vue4
-rw-r--r--src/components/status/status.js8
-rw-r--r--src/components/timeline/timeline.js13
-rw-r--r--src/components/user_card/user_card.js64
-rw-r--r--src/components/user_card/user_card.vue159
-rw-r--r--src/components/user_card_content/user_card_content.vue27
-rw-r--r--src/components/user_profile/user_profile.js48
-rw-r--r--src/components/user_profile/user_profile.vue13
-rw-r--r--src/components/user_search/user_search.js4
-rw-r--r--src/components/user_search/user_search.vue2
-rw-r--r--src/components/user_settings/user_settings.js18
-rw-r--r--src/components/user_settings/user_settings.vue36
-rw-r--r--src/components/who_to_follow/who_to_follow.js4
-rw-r--r--src/components/who_to_follow/who_to_follow.vue2
-rw-r--r--src/hocs/with_load_more/with_load_more.js11
-rw-r--r--src/hocs/with_subscription/with_subscription.js12
-rw-r--r--src/i18n/ar.json5
-rw-r--r--src/i18n/ca.json5
-rw-r--r--src/i18n/de.json5
-rw-r--r--src/i18n/en.json11
-rw-r--r--src/i18n/es.json5
-rw-r--r--src/i18n/fi.json6
-rw-r--r--src/i18n/fr.json5
-rw-r--r--src/i18n/ga.json5
-rw-r--r--src/i18n/he.json5
-rw-r--r--src/i18n/it.json5
-rw-r--r--src/i18n/ja.json5
-rw-r--r--src/i18n/ko.json5
-rw-r--r--src/i18n/nb.json5
-rw-r--r--src/i18n/nl.json5
-rw-r--r--src/i18n/oc.json1
-rw-r--r--src/i18n/pl.json5
-rw-r--r--src/i18n/ru.json5
-rw-r--r--src/i18n/zh.json5
-rw-r--r--src/main.js4
-rw-r--r--src/modules/config.js1
-rw-r--r--src/modules/oauth_tokens.js26
-rw-r--r--src/modules/users.js39
-rw-r--r--src/services/api/api.service.js30
-rw-r--r--src/services/backend_interactor_service/backend_interactor_service.js4
-rw-r--r--src/services/component_utils/component_utils.js10
-rw-r--r--src/services/errors/errors.js14
-rw-r--r--src/services/follow_request_fetcher/follow_request_fetcher.service.js21
58 files changed, 597 insertions, 431 deletions
diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 4ede15e9..77fb0aa0 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -1,26 +1,22 @@
<template>
<div class="user-card">
<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>
<div class="user-card-expanded-content" v-if="userExpanded">
<user-card-content :user="user" :switcher="false"></user-card-content>
</div>
<div class="user-card-collapsed-content" v-else>
- <div class="user-card-primary-area">
- <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>
- <router-link class='user-screen-name' :to="userProfileLink(user)">
- @{{user.screen_name}}
- </router-link>
- </div>
+ <div :title="user.name" class="user-card-user-name">
+ <span v-if="user.name_html" v-html="user.name_html"></span>
+ <span v-else>{{ user.name }}</span>
</div>
- <div class="user-card-secondary-area">
- <slot name="secondary-area"></slot>
+ <div>
+ <router-link class="user-card-screen-name" :to="userProfileLink(user)">
+ @{{user.screen_name}}
+ </router-link>
</div>
+ <slot></slot>
</div>
</div>
</template>
@@ -46,30 +42,21 @@
margin-left: 0.7em;
text-align: left;
flex: 1;
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
+ min-width: 0;
}
- &-primary-area {
- flex: 1;
- .user-name {
- img {
- object-fit: contain;
- height: 16px;
- width: 16px;
- vertical-align: middle;
- }
+ &-user-name {
+ img {
+ object-fit: contain;
+ height: 16px;
+ width: 16px;
+ vertical-align: middle;
}
}
- &-secondary-area {
- flex: none;
- }
-
&-expanded-content {
flex: 1;
- margin: 0.2em 0 0 0.7em;
+ margin-left: 0.7em;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
diff --git a/src/components/block_card/block_card.vue b/src/components/block_card/block_card.vue
index ed7fe30b..8eb56e25 100644
--- a/src/components/block_card/block_card.vue
+++ b/src/components/block_card/block_card.vue
@@ -1,6 +1,6 @@
<template>
<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">
<template v-if="progress">
{{ $t('user_card.unblock_progress') }}
@@ -17,8 +17,18 @@
{{ $t('user_card.block') }}
</template>
</button>
- </template>
+ </div>
</basic-user-card>
</template>
-<script src="./block_card.js"></script> \ No newline at end of file
+<script src="./block_card.js"></script>
+
+<style lang="scss">
+.block-card-content-container {
+ margin-top: 0.5em;
+ text-align: right;
+ button {
+ width: 10em;
+ }
+}
+</style>
diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js
new file mode 100644
index 00000000..425c9c3e
--- /dev/null
+++ b/src/components/follow_card/follow_card.js
@@ -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
diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue
new file mode 100644
index 00000000..6cb064eb
--- /dev/null
+++ b/src/components/follow_card/follow_card.vue
@@ -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>
diff --git a/src/components/follow_list/follow_list.js b/src/components/follow_list/follow_list.js
deleted file mode 100644
index 9777c87e..00000000
--- a/src/components/follow_list/follow_list.js
+++ /dev/null
@@ -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
diff --git a/src/components/follow_list/follow_list.vue b/src/components/follow_list/follow_list.vue
deleted file mode 100644
index 27102edf..00000000
--- a/src/components/follow_list/follow_list.vue
+++ /dev/null
@@ -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>
diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js
new file mode 100644
index 00000000..1a00a1c1
--- /dev/null
+++ b/src/components/follow_request_card/follow_request_card.js
@@ -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
diff --git a/src/components/follow_request_card/follow_request_card.vue b/src/components/follow_request_card/follow_request_card.vue
new file mode 100644
index 00000000..4a3bbba4
--- /dev/null
+++ b/src/components/follow_request_card/follow_request_card.vue
@@ -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>
diff --git a/src/components/follow_requests/follow_requests.js b/src/components/follow_requests/follow_requests.js
index 11a228aa..704a76c6 100644
--- a/src/components/follow_requests/follow_requests.js
+++ b/src/components/follow_requests/follow_requests.js
@@ -1,22 +1,13 @@
-import UserCard from '../user_card/user_card.vue'
+import FollowRequestCard from '../follow_request_card/follow_request_card.vue'
const FollowRequests = {
components: {
- UserCard
- },
- created () {
- this.updateRequests()
+ FollowRequestCard
},
computed: {
requests () {
return this.$store.state.api.followRequests
}
- },
- methods: {
- updateRequests () {
- this.$store.state.api.backendInteractor.fetchFollowRequests()
- .then((requests) => { this.$store.commit('setFollowRequests', requests) })
- }
}
}
diff --git a/src/components/follow_requests/follow_requests.vue b/src/components/follow_requests/follow_requests.vue
index 87dc4194..b83c2d68 100644
--- a/src/components/follow_requests/follow_requests.vue
+++ b/src/components/follow_requests/follow_requests.vue
@@ -4,7 +4,7 @@
{{$t('nav.friend_requests')}}
</div>
<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>
</template>
diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js
index ea5d7ea4..aa3f7605 100644
--- a/src/components/nav_panel/nav_panel.js
+++ b/src/components/nav_panel/nav_panel.js
@@ -1,10 +1,23 @@
+import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
+
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: {
currentUser () {
return this.$store.state.users.currentUser
},
chat () {
return this.$store.state.chat.channel
+ },
+ followRequestCount () {
+ return this.$store.state.api.followRequests.length
}
}
}
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index 1a269adf..7a7212fb 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -20,8 +20,8 @@
<li v-if='currentUser && currentUser.locked'>
<router-link :to="{ name: 'friend-requests' }">
{{ $t("nav.friend_requests")}}
- <span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count">
- {{currentUser.follow_request_count}}
+ <span v-if='followRequestCount > 0' class="badge follow-request-count">
+ {{followRequestCount}}
</span>
</router-link>
</li>
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index 23c1acdb..6e2dff7b 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -12,6 +12,7 @@ const settings = {
return {
hideAttachmentsLocal: user.hideAttachments,
hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
+ maxThumbnails: user.maxThumbnails,
hideNsfwLocal: user.hideNsfw,
useOneClickNsfw: user.useOneClickNsfw,
hideISPLocal: user.hideISP,
@@ -186,6 +187,10 @@ const settings = {
},
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 })
}
}
}
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index f5e00995..5041b3a3 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -137,6 +137,10 @@
<label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label>
</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>
<input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
<label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
</li>
@@ -146,7 +150,7 @@
<label for="preloadImage">{{$t('settings.preload_images')}}</label>
</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>
</li>
</ul>
@@ -316,6 +320,10 @@
min-width: 10em;
padding: 0 2em;
}
+
+ .number-input {
+ max-width: 6em;
+ }
}
.select-multiple {
display: flex;
diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js
index 40ffa1dd..b5c49059 100644
--- a/src/components/side_drawer/side_drawer.js
+++ b/src/components/side_drawer/side_drawer.js
@@ -32,6 +32,9 @@ const SideDrawer = {
},
sitename () {
return this.$store.state.instance.name
+ },
+ followRequestCount () {
+ return this.$store.state.api.followRequests.length
}
},
methods: {
diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue
index 8eca7b8c..6996380d 100644
--- a/src/components/side_drawer/side_drawer.vue
+++ b/src/components/side_drawer/side_drawer.vue
@@ -45,8 +45,8 @@
<li v-if="currentUser && currentUser.locked" @click="toggleDrawer">
<router-link to='/friend-requests'>
{{ $t("nav.friend_requests") }}
- <span v-if='currentUser.follow_request_count > 0' class="badge follow-request-count">
- {{currentUser.follow_request_count}}
+ <span v-if='followRequestCount > 0' class="badge follow-request-count">
+ {{followRequestCount}}
</span>
</router-link>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 0273a5be..fab2fe62 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -40,8 +40,7 @@ const Status = {
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
? !this.$store.state.instance.collapseMessageWithSubject
: !this.$store.state.config.collapseMessageWithSubject,
- betterShadow: this.$store.state.interface.browserSupport.cssFilter,
- maxAttachments: 9
+ betterShadow: this.$store.state.interface.browserSupport.cssFilter
}
},
computed: {
@@ -225,7 +224,7 @@ const Status = {
attachmentSize () {
if ((this.$store.state.config.hideAttachments && !this.inConversation) ||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation) ||
- (this.status.attachments.length > this.maxAttachments)) {
+ (this.status.attachments.length > this.maxThumbnails)) {
return 'hide'
} else if (this.compact) {
return 'small'
@@ -249,6 +248,9 @@ const Status = {
return this.status.attachments.filter(
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
)
+ },
+ maxThumbnails () {
+ return this.$store.state.config.maxThumbnails
}
},
components: {
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 25c553cb..655bfb3f 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -1,7 +1,6 @@
import Status from '../status/status.vue'
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue'
-import UserCard from '../user_card/user_card.vue'
import { throttle } from 'lodash'
const Timeline = {
@@ -44,8 +43,7 @@ const Timeline = {
},
components: {
Status,
- StatusOrConversation,
- UserCard
+ StatusOrConversation
},
created () {
const store = this.$store
@@ -70,14 +68,21 @@ const Timeline = {
document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
this.unfocused = document.hidden
}
+ window.addEventListener('keydown', this.handleShortKey)
},
destroyed () {
window.removeEventListener('scroll', this.scrollLoad)
+ window.removeEventListener('keydown', this.handleShortKey)
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
},
methods: {
+ handleShortKey (e) {
+ if (e.key === '.') this.showNewStatuses()
+ },
showNewStatuses () {
+ if (this.newStatusCount === 0) return
+
if (this.timeline.flushMarker !== 0) {
this.$store.commit('clearTimeline', { timeline: this.timelineName })
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
@@ -101,7 +106,7 @@ const Timeline = {
tag: this.tag
}).then(statuses => {
store.commit('setLoading', { timeline: this.timelineName, value: false })
- if (statuses.length === 0) {
+ if (statuses && statuses.length === 0) {
this.bottomedOut = true
}
})
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
deleted file mode 100644
index 28e22f09..00000000
--- a/src/components/user_card/user_card.js
+++ /dev/null
@@ -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
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
deleted file mode 100644
index ce4edb3c..00000000
--- a/src/components/user_card/user_card.vue
+++ /dev/null
@@ -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>
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index a3d24eb1..689b9ec6 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -222,6 +222,13 @@
overflow: hidden;
flex: 1 1 auto;
margin-right: 1em;
+
+ img {
+ object-fit: contain;
+ height: 16px;
+ width: 16px;
+ vertical-align: middle;
+ }
}
.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>
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 37179ce1..7708141c 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -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 UserCard from '../user_card/user_card.vue'
+import FollowCard from '../follow_card/follow_card.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 = {
+ data () {
+ return {
+ error: false
+ }
+ },
created () {
this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.commit('clearTimeline', { timeline: 'favorites' })
@@ -13,6 +43,16 @@ const UserProfile = {
this.startFetchFavorites()
if (!this.user.id) {
this.$store.dispatch('fetchUser', this.fetchBy)
+ .catch((reason) => {
+ const errorMessage = get(reason, 'error.error')
+ if (errorMessage === 'No user with such user_id') { // Known error
+ this.error = this.$t('user_profile.profile_does_not_exist')
+ } else if (errorMessage) {
+ this.error = errorMessage
+ } else {
+ this.error = this.$t('user_profile.profile_loading_error')
+ }
+ })
}
},
destroyed () {
@@ -105,9 +145,9 @@ const UserProfile = {
},
components: {
UserCardContent,
- UserCard,
Timeline,
- FollowList
+ FollowerList,
+ FriendList
}
}
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 09fb93de..a3d2825f 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -18,16 +18,10 @@
:user-id="fetchBy"
/>
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
- <FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" />
- <div class="userlist-placeholder" v-else>
- <i class="icon-spin3 animate-spin"></i>
- </div>
+ <FriendList :userId="userId" />
</div>
<div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
- <FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" />
- <div class="userlist-placeholder" v-else>
- <i class="icon-spin3 animate-spin"></i>
- </div>
+ <FollowerList :userId="userId" :entryProps="{noFollowsYou: isUs}" />
</div>
<Timeline
:label="$t('user_card.media')"
@@ -55,7 +49,8 @@
</div>
</div>
<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>
diff --git a/src/components/user_search/user_search.js b/src/components/user_search/user_search.js
index fe67b2ad..55040826 100644
--- a/src/components/user_search/user_search.js
+++ b/src/components/user_search/user_search.js
@@ -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'
const userSearch = {
components: {
- UserCard
+ FollowCard
},
props: [
'query'
diff --git a/src/components/user_search/user_search.vue b/src/components/user_search/user_search.vue
index b39e10f4..1269eea6 100644
--- a/src/components/user_search/user_search.vue
+++ b/src/components/user_search/user_search.vue
@@ -13,7 +13,7 @@
<i class="icon-spin3 animate-spin"/>
</div>
<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>
</template>
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index 06e72112..d6972737 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -1,7 +1,6 @@
import { compose } from 'vue-compose'
import unescape from 'lodash/unescape'
import get from 'lodash/get'
-
import TabSwitcher from '../tab_switcher/tab_switcher.js'
import ImageCropper from '../image_cropper/image_cropper.vue'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
@@ -62,6 +61,9 @@ const UserSettings = {
activeTab: 'profile'
}
},
+ created () {
+ this.$store.dispatch('fetchTokens')
+ },
components: {
StyleSwitcher,
TabSwitcher,
@@ -89,6 +91,15 @@ const UserSettings = {
},
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: {
@@ -308,6 +319,11 @@ const UserSettings = {
logout () {
this.$store.dispatch('logout')
this.$router.replace('/')
+ },
+ revokeToken (id) {
+ if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
+ this.$store.dispatch('revokeToken', id)
+ }
}
}
}
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 983cbda0..a1123638 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -122,6 +122,30 @@
</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">
<h2>{{$t('settings.delete_account')}}</h2>
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
<div v-if="deletingAccount">
@@ -213,5 +237,17 @@
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
}
+
+ .oauth-tokens {
+ width: 100%;
+
+ th {
+ text-align: left;
+ }
+
+ .actions {
+ text-align: right;
+ }
+ }
}
</style>
diff --git a/src/components/who_to_follow/who_to_follow.js b/src/components/who_to_follow/who_to_follow.js
index 82098fc2..be0b8827 100644
--- a/src/components/who_to_follow/who_to_follow.js
+++ b/src/components/who_to_follow/who_to_follow.js
@@ -1,9 +1,9 @@
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 = {
components: {
- UserCard
+ FollowCard
},
data () {
return {
diff --git a/src/components/who_to_follow/who_to_follow.vue b/src/components/who_to_follow/who_to_follow.vue
index df2e03c8..1630f5ac 100644
--- a/src/components/who_to_follow/who_to_follow.vue
+++ b/src/components/who_to_follow/who_to_follow.vue
@@ -4,7 +4,7 @@
{{$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>
+ <FollowCard v-for="user in users" :key="user.id" :user="user"/>
</div>
</div>
</template>
diff --git a/src/hocs/with_load_more/with_load_more.js b/src/hocs/with_load_more/with_load_more.js
index e862a39b..74979b87 100644
--- a/src/hocs/with_load_more/with_load_more.js
+++ b/src/hocs/with_load_more/with_load_more.js
@@ -1,15 +1,17 @@
import Vue from 'vue'
-import filter from 'lodash/filter'
import isEmpty from 'lodash/isEmpty'
+import { getComponentProps } from '../../services/component_utils/component_utils'
import './with_load_more.scss'
const withLoadMore = ({
fetch, // function to fetch entries and return a promise
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) => {
- const originalProps = WrappedComponent.props || []
- const props = filter(originalProps, v => v !== 'entries')
+ const originalProps = Object.keys(getComponentProps(WrappedComponent))
+ const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames)
return Vue.component('withLoadMore', {
render (createElement) {
@@ -56,6 +58,7 @@ const withLoadMore = ({
},
destroyed () {
window.removeEventListener('scroll', this.scrollLoad)
+ destroy && destroy(this.$props, this.$store)
},
methods: {
fetchEntries () {
diff --git a/src/hocs/with_subscription/with_subscription.js b/src/hocs/with_subscription/with_subscription.js
index 1ac67cba..679409cf 100644
--- a/src/hocs/with_subscription/with_subscription.js
+++ b/src/hocs/with_subscription/with_subscription.js
@@ -1,16 +1,16 @@
import Vue from 'vue'
-import reject from 'lodash/reject'
import isEmpty from 'lodash/isEmpty'
-import omit from 'lodash/omit'
+import { getComponentProps } from '../../services/component_utils/component_utils'
import './with_subscription.scss'
const withSubscription = ({
fetch, // function to fetch entries and return a promise
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) => {
- const originalProps = WrappedComponent.props || []
- const props = reject(originalProps, v => v === 'content')
+ const originalProps = Object.keys(getComponentProps(WrappedComponent))
+ const props = originalProps.filter(v => v !== childPropName).concat(additionalPropNames)
return Vue.component('withSubscription', {
props: [
@@ -21,7 +21,7 @@ const withSubscription = ({
if (!this.error && !this.loading) {
const props = {
props: {
- ...omit(this.$props, 'refresh'),
+ ...this.$props,
[childPropName]: this.fetchedData
},
on: this.$listeners,
diff --git a/src/i18n/ar.json b/src/i18n/ar.json
index ac7d0f1a..242dab78 100644
--- a/src/i18n/ar.json
+++ b/src/i18n/ar.json
@@ -134,6 +134,11 @@
"notification_visibility_mentions": "الإشارات",
"notification_visibility_repeats": "",
"nsfw_clickthrough": "",
+ "oauth_tokens": "رموز OAuth",
+ "token": "رمز",
+ "refresh_token": "رمز التحديث",
+ "valid_until": "صالح حتى",
+ "revoke_token": "سحب",
"panelRadius": "",
"pause_on_unfocused": "",
"presets": "النماذج",
diff --git a/src/i18n/ca.json b/src/i18n/ca.json
index fa517e22..d2f285df 100644
--- a/src/i18n/ca.json
+++ b/src/i18n/ca.json
@@ -132,6 +132,11 @@
"notification_visibility_repeats": "Republica una entrada meva",
"no_rich_text_description": "Neteja el formatat de text de totes les entrades",
"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",
"pause_on_unfocused": "Pausa la reproducció en continu quan la pestanya perdi el focus",
"presets": "Temes",
diff --git a/src/i18n/de.json b/src/i18n/de.json
index d0bfba38..07d44348 100644
--- a/src/i18n/de.json
+++ b/src/i18n/de.json
@@ -159,6 +159,11 @@
"hide_follows_description": "Zeige nicht, wem ich folge",
"hide_followers_description": "Zeige nicht, wer mir folgt",
"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",
"pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist",
"presets": "Voreinstellungen",
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 64753f1d..c3756374 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -106,6 +106,7 @@
}
},
"settings": {
+ "app_name": "App name",
"attachmentRadius": "Attachments",
"attachments": "Attachments",
"autoload": "Enable automatic loading when scrolled to the bottom",
@@ -149,6 +150,7 @@
"general": "General",
"hide_attachments_in_convo": "Hide attachments in conversations",
"hide_attachments_in_tl": "Hide attachments in timeline",
+ "max_thumbnails": "Maximum amount of thumbnails per post",
"hide_isp": "Hide instance-specific panel",
"preload_images": "Preload images",
"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_moderator_badge": "Show Moderator badge in my profile",
"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",
"pause_on_unfocused": "Pause streaming when tab is not focused",
"presets": "Presets",
@@ -383,7 +390,9 @@
"mute_progress": "Muting..."
},
"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": {
"more": "More",
diff --git a/src/i18n/es.json b/src/i18n/es.json
index d14e7a31..167e8c42 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -171,6 +171,11 @@
"show_admin_badge": "Mostrar la placa de administrador en mi perfil",
"show_moderator_badge": "Mostrar la placa de moderador en mi perfil",
"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",
"pause_on_unfocused": "Parar la transmisión cuando no estés en foco.",
"presets": "Por defecto",
diff --git a/src/i18n/fi.json b/src/i18n/fi.json
index 5a0c1ea8..c7a25fe1 100644
--- a/src/i18n/fi.json
+++ b/src/i18n/fi.json
@@ -133,6 +133,7 @@
"general": "Yleinen",
"hide_attachments_in_convo": "Piilota liitteet keskusteluissa",
"hide_attachments_in_tl": "Piilota liitteet aikajanalla",
+ "max_thumbnails": "Suurin sallittu määrä liitteitä esikatselussa",
"hide_isp": "Piilota palvelimenkohtainen ruutu",
"preload_images": "Esilataa kuvat",
"use_one_click_nsfw": "Avaa NSFW-liitteet yhdellä painalluksella",
@@ -165,6 +166,11 @@
"no_rich_text_description": "Älä näytä tekstin muotoilua.",
"hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
"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",
"pause_on_unfocused": "Pysäytä automaattinen viestien näyttö välilehden ollessa pois fokuksesta",
"presets": "Valmiit teemat",
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index 129b7d7c..1209556a 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -137,6 +137,11 @@
"notification_visibility_mentions": "Mentionnés",
"notification_visibility_repeats": "Partages",
"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",
"pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas centré",
"presets": "Thèmes prédéfinis",
diff --git a/src/i18n/ga.json b/src/i18n/ga.json
index 64461202..5be9297a 100644
--- a/src/i18n/ga.json
+++ b/src/i18n/ga.json
@@ -134,6 +134,11 @@
"notification_visibility_repeats": "Atphostáil",
"no_rich_text_description": "Bain formáidiú téacs saibhir ó gach post",
"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",
"pause_on_unfocused": "Sruthú ar sos nuair a bhíonn an fócas caillte",
"presets": "Réamhshocruithe",
diff --git a/src/i18n/he.json b/src/i18n/he.json
index 99ae9551..213e6170 100644
--- a/src/i18n/he.json
+++ b/src/i18n/he.json
@@ -129,6 +129,11 @@
"notification_visibility_mentions": "אזכורים",
"notification_visibility_repeats": "חזרות",
"nsfw_clickthrough": "החל החבאת צירופים לא בטוחים לצפיה בעת עבודה בעזרת לחיצת עכבר",
+ "oauth_tokens": "אסימוני OAuth",
+ "token": "אסימון",
+ "refresh_token": "רענון האסימון",
+ "valid_until": "בתוקף עד",
+ "revoke_token": "בטל",
"panelRadius": "פאנלים",
"pause_on_unfocused": "השהה זרימת הודעות כשהחלון לא בפוקוס",
"presets": "ערכים קבועים מראש",
diff --git a/src/i18n/it.json b/src/i18n/it.json
index 8f69e7c1..385d21aa 100644
--- a/src/i18n/it.json
+++ b/src/i18n/it.json
@@ -93,6 +93,11 @@
"notification_visibility_mentions": "Menzioni",
"notification_visibility_repeats": "Condivisioni",
"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",
"pause_on_unfocused": "Metti in pausa l'aggiornamento continuo quando la scheda non è in primo piano",
"presets": "Valori predefiniti",
diff --git a/src/i18n/ja.json b/src/i18n/ja.json
index 7849aa20..b51fa7fd 100644
--- a/src/i18n/ja.json
+++ b/src/i18n/ja.json
@@ -171,6 +171,11 @@
"show_admin_badge": "アドミンのしるしをみる",
"show_moderator_badge": "モデレーターのしるしをみる",
"nsfw_clickthrough": "NSFWなファイルをかくす",
+ "oauth_tokens": "OAuthトークン",
+ "token": "トークン",
+ "refresh_token": "トークンを更新",
+ "valid_until": "まで有効",
+ "revoke_token": "取り消す",
"panelRadius": "パネル",
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
"presets": "プリセット",
diff --git a/src/i18n/ko.json b/src/i18n/ko.json
index f9e4dfa3..336e464f 100644
--- a/src/i18n/ko.json
+++ b/src/i18n/ko.json
@@ -159,6 +159,11 @@
"hide_follows_description": "내가 팔로우하는 사람을 표시하지 않음",
"hide_followers_description": "나를 따르는 사람을 보여주지 마라.",
"nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화",
+ "oauth_tokens": "OAuth 토큰",
+ "token": "토큰",
+ "refresh_token": "토큰 새로 고침",
+ "valid_until": "까지 유효하다",
+ "revoke_token": "취소",
"panelRadius": "패널",
"pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기",
"presets": "프리셋",
diff --git a/src/i18n/nb.json b/src/i18n/nb.json
index 0f4dca58..39e054f7 100644
--- a/src/i18n/nb.json
+++ b/src/i18n/nb.json
@@ -132,6 +132,11 @@
"notification_visibility_repeats": "Gjentakelser",
"no_rich_text_description": "Fjern all formatering fra statuser",
"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",
"pause_on_unfocused": "Stopp henting av poster når vinduet ikke er i fokus",
"presets": "Forhåndsdefinerte tema",
diff --git a/src/i18n/nl.json b/src/i18n/nl.json
index bb388a90..799e22b9 100644
--- a/src/i18n/nl.json
+++ b/src/i18n/nl.json
@@ -159,6 +159,11 @@
"no_rich_text_description": "Strip rich text formattering van alle posts",
"hide_network_description": "Toon niet wie mij volgt en wie ik volg.",
"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",
"pause_on_unfocused": "Pauzeer streamen wanneer de tab niet gefocused is",
"presets": "Presets",
diff --git a/src/i18n/oc.json b/src/i18n/oc.json
index 2ce666c6..db66bb98 100644
--- a/src/i18n/oc.json
+++ b/src/i18n/oc.json
@@ -142,6 +142,7 @@
"notification_visibility_mentions": "Mencions",
"notification_visibility_repeats": "Repeticions",
"no_rich_text_description": "Netejar lo format tèxte de totas las publicacions",
+ "oauth_tokens": "Llistats OAuth",
"pause_on_unfocused": "Pausar la difusion quand l’onglet es pas seleccionat",
"profile_tab": "Perfil",
"replies_in_timeline": "Responsas del flux",
diff --git a/src/i18n/pl.json b/src/i18n/pl.json
index a3952d4f..2e1d7488 100644
--- a/src/i18n/pl.json
+++ b/src/i18n/pl.json
@@ -86,6 +86,11 @@
"name_bio": "Imię i bio",
"new_password": "Nowe hasło",
"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",
"presets": "Gotowe motywy",
"profile_background": "Tło profilu",
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 4b0bd4b4..6799cc96 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -132,6 +132,11 @@
"show_admin_badge": "Показывать значок администратора в моем профиле",
"show_moderator_badge": "Показывать значок модератора в моем профиле",
"nsfw_clickthrough": "Включить скрытие NSFW вложений",
+ "oauth_tokens": "OAuth токены",
+ "token": "Токен",
+ "refresh_token": "Рефреш токен",
+ "valid_until": "Годен до",
+ "revoke_token": "Удалить",
"panelRadius": "Панели",
"pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",
"presets": "Пресеты",
diff --git a/src/i18n/zh.json b/src/i18n/zh.json
index 7ad23c57..089a98e2 100644
--- a/src/i18n/zh.json
+++ b/src/i18n/zh.json
@@ -134,6 +134,11 @@
"notification_visibility_repeats": "转发",
"no_rich_text_description": "不显示富文本格式",
"nsfw_clickthrough": "将不和谐附件隐藏,点击才能打开",
+ "oauth_tokens": "OAuth令牌",
+ "token": "代币",
+ "refresh_token": "刷新令牌",
+ "valid_until": "有效期至",
+ "revoke_token": "撤消",
"panelRadius": "面板",
"pause_on_unfocused": "在离开页面时暂停时间线推送",
"presets": "预置",
diff --git a/src/main.js b/src/main.js
index adeb0550..2844194e 100644
--- a/src/main.js
+++ b/src/main.js
@@ -11,6 +11,7 @@ import configModule from './modules/config.js'
import chatModule from './modules/chat.js'
import oauthModule from './modules/oauth.js'
import mediaViewerModule from './modules/media_viewer.js'
+import oauthTokensModule from './modules/oauth_tokens.js'
import VueTimeago from 'vue-timeago'
import VueI18n from 'vue-i18n'
@@ -64,7 +65,8 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
config: configModule,
chat: chatModule,
oauth: oauthModule,
- mediaViewer: mediaViewerModule
+ mediaViewer: mediaViewerModule,
+ oauthTokens: oauthTokensModule
},
plugins: [persistedState, pushNotifications],
strict: false // Socket modifies itself, let's ignore this for now.
diff --git a/src/modules/config.js b/src/modules/config.js
index 71f71376..1c30c203 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -8,6 +8,7 @@ const defaultState = {
collapseMessageWithSubject: undefined, // instance default
hideAttachments: false,
hideAttachmentsInConv: false,
+ maxThumbnails: 16,
hideNsfw: true,
preloadImage: true,
loopVideo: true,
diff --git a/src/modules/oauth_tokens.js b/src/modules/oauth_tokens.js
new file mode 100644
index 00000000..00ac1431
--- /dev/null
+++ b/src/modules/oauth_tokens.js
@@ -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
diff --git a/src/modules/users.js b/src/modules/users.js
index 77df7168..093af497 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -72,14 +72,20 @@ export const mutations = {
},
// Because frontend doesn't have a reason to keep these stuff in memory
// outside of viewing someones user profile.
- clearFriendsAndFollowers (state, userKey) {
- const user = state.usersObject[userKey]
+ clearFriends (state, userId) {
+ const user = state.usersObject[userId]
if (!user) {
return
}
user.friends = []
- user.followers = []
user.friendsPage = 0
+ },
+ clearFollowers (state, userId) {
+ const user = state.usersObject[userId]
+ if (!user) {
+ return
+ }
+ user.followers = []
user.followersPage = 0
},
addNewUsers (state, users) {
@@ -140,7 +146,7 @@ const users = {
getters,
actions: {
fetchUser (store, id) {
- store.rootState.api.backendInteractor.fetchUser({ id })
+ return store.rootState.api.backendInteractor.fetchUser({ id })
.then((user) => store.commit('addNewUsers', [user]))
},
fetchBlocks (store) {
@@ -189,20 +195,19 @@ const users = {
})
},
addFollowers ({ rootState, commit }, fetchBy) {
- return new Promise((resolve, reject) => {
- const user = rootState.users.usersObject[fetchBy]
- const page = user.followersPage || 1
- rootState.api.backendInteractor.fetchFollowers({ id: user.id, page })
- .then((followers) => {
- commit('addFollowers', { id: user.id, followers, page })
- resolve(followers)
- }).catch(() => {
- reject()
- })
- })
+ const user = rootState.users.usersObject[fetchBy]
+ const page = user.followersPage || 1
+ return rootState.api.backendInteractor.fetchFollowers({ id: user.id, page })
+ .then((followers) => {
+ commit('addFollowers', { id: user.id, followers, page })
+ return followers
+ })
+ },
+ clearFriends ({ commit }, userId) {
+ commit('clearFriends', userId)
},
- clearFriendsAndFollowers ({ commit }, userKey) {
- commit('clearFriendsAndFollowers', userKey)
+ clearFollowers ({ commit }, userId) {
+ commit('clearFollowers', userId)
},
registerPushNotifications (store) {
const token = store.state.currentUser.credentials
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 3d2e8823..2de87026 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -47,6 +47,7 @@ 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'
+import { StatusCodeError } from '../errors/errors'
const oldfetch = window.fetch
@@ -244,7 +245,15 @@ const denyUser = ({id, credentials}) => {
const fetchUser = ({id, credentials}) => {
let url = `${USER_URL}?user_id=${id}`
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))
}
@@ -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}) => {
return fetch(SUGGESTIONS_URL, {
headers: authHeaders(credentials)
@@ -573,6 +599,8 @@ const apiService = {
setUserMute,
fetchMutes,
fetchBlocks,
+ fetchOAuthTokens,
+ revokeOAuthToken,
register,
getCaptcha,
updateAvatar,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 2df61495..7e972d7b 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -65,6 +65,8 @@ const backendInteractorService = (credentials) => {
const fetchMutes = () => apiService.fetchMutes({credentials})
const fetchBlocks = (params) => apiService.fetchBlocks({credentials, ...params})
const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials})
+ const fetchOAuthTokens = () => apiService.fetchOAuthTokens({credentials})
+ const revokeOAuthToken = (id) => apiService.revokeOAuthToken({id, credentials})
const getCaptcha = () => apiService.getCaptcha()
const register = (params) => apiService.register(params)
@@ -96,6 +98,8 @@ const backendInteractorService = (credentials) => {
setUserMute,
fetchMutes,
fetchBlocks,
+ fetchOAuthTokens,
+ revokeOAuthToken,
register,
getCaptcha,
updateAvatar,
diff --git a/src/services/component_utils/component_utils.js b/src/services/component_utils/component_utils.js
new file mode 100644
index 00000000..77ea14a1
--- /dev/null
+++ b/src/services/component_utils/component_utils.js
@@ -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
+}
diff --git a/src/services/errors/errors.js b/src/services/errors/errors.js
new file mode 100644
index 00000000..548f3c68
--- /dev/null
+++ b/src/services/errors/errors.js
@@ -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
diff --git a/src/services/follow_request_fetcher/follow_request_fetcher.service.js b/src/services/follow_request_fetcher/follow_request_fetcher.service.js
new file mode 100644
index 00000000..125ff3e1
--- /dev/null
+++ b/src/services/follow_request_fetcher/follow_request_fetcher.service.js
@@ -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