aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
authorHenry Jameson <me@hjkos.com>2019-04-27 12:26:17 +0300
committerHenry Jameson <me@hjkos.com>2019-04-27 12:26:17 +0300
commite0247e21f6fad72481d8f04271d3a15e0d827acc (patch)
treeacc8a11074865a9cbdc50e9b52da918c48f7235e /src/components
parent2b5cf61a8f9a84467495721f170df7ae1288959b (diff)
parent9e2fa50b74eb83e3c3eadb9a68d24ddaa1d9da48 (diff)
Merge remote-tracking branch 'upstream/develop' into webpack-4-dart-sass
* upstream/develop: (126 commits) entity normalizer: hook up in_reply_to_account_acct add BBCode strings fix follow button not updating bug in follow-card refer searched user objects from the global user rep set max-width of textarea in settings page Remove space in the timeline setting copy user_card.vue: Fix .emoji to apply to img Update oc.json Update oc.json Update oc.json Update oc.json replace pencil with wrench icon rebuild fontello with wrench icon added fetch all friends using pagination stop fetching user relationship when user is unauthorized Revert "recover border between basic-user-card using list component" remove extra spacing code readability fix typos clean up ...
Diffstat (limited to 'src/components')
-rw-r--r--src/components/autosuggest/autosuggest.js52
-rw-r--r--src/components/autosuggest/autosuggest.vue45
-rw-r--r--src/components/basic_user_card/basic_user_card.vue10
-rw-r--r--src/components/checkbox/checkbox.vue75
-rw-r--r--src/components/conversation/conversation.js33
-rw-r--r--src/components/conversation/conversation.vue2
-rw-r--r--src/components/delete_button/delete_button.js6
-rw-r--r--src/components/dialog_modal/dialog_modal.js14
-rw-r--r--src/components/dialog_modal/dialog_modal.vue92
-rw-r--r--src/components/follow_card/follow_card.js15
-rw-r--r--src/components/follow_card/follow_card.vue60
-rw-r--r--src/components/follow_requests/follow_requests.vue2
-rw-r--r--src/components/list/list.vue42
-rw-r--r--src/components/login_form/login_form.js10
-rw-r--r--src/components/moderation_tools/moderation_tools.js106
-rw-r--r--src/components/moderation_tools/moderation_tools.vue158
-rw-r--r--src/components/notification/notification.js11
-rw-r--r--src/components/notification/notification.vue32
-rw-r--r--src/components/notifications/notifications.js9
-rw-r--r--src/components/notifications/notifications.vue2
-rw-r--r--src/components/popper/popper.scss70
-rw-r--r--src/components/progress_button/progress_button.vue35
-rw-r--r--src/components/public_and_external_timeline/public_and_external_timeline.js2
-rw-r--r--src/components/public_timeline/public_timeline.js2
-rw-r--r--src/components/selectable_list/selectable_list.js66
-rw-r--r--src/components/selectable_list/selectable_list.vue59
-rw-r--r--src/components/settings/settings.vue5
-rw-r--r--src/components/tag_timeline/tag_timeline.js4
-rw-r--r--src/components/timeline/timeline.js2
-rw-r--r--src/components/user_card/user_card.js13
-rw-r--r--src/components/user_card/user_card.vue6
-rw-r--r--src/components/user_profile/user_profile.js133
-rw-r--r--src/components/user_profile/user_profile.vue14
-rw-r--r--src/components/user_search/user_search.js14
-rw-r--r--src/components/user_search/user_search.vue2
-rw-r--r--src/components/user_settings/user_settings.js74
-rw-r--r--src/components/user_settings/user_settings.vue60
-rw-r--r--src/components/who_to_follow/who_to_follow.vue2
38 files changed, 1111 insertions, 228 deletions
diff --git a/src/components/autosuggest/autosuggest.js b/src/components/autosuggest/autosuggest.js
new file mode 100644
index 00000000..d4efe912
--- /dev/null
+++ b/src/components/autosuggest/autosuggest.js
@@ -0,0 +1,52 @@
+const debounceMilliseconds = 500
+
+export default {
+ props: {
+ query: { // function to query results and return a promise
+ type: Function,
+ required: true
+ },
+ filter: { // function to filter results in real time
+ type: Function
+ },
+ placeholder: {
+ type: String,
+ default: 'Search...'
+ }
+ },
+ data () {
+ return {
+ term: '',
+ timeout: null,
+ results: [],
+ resultsVisible: false
+ }
+ },
+ computed: {
+ filtered () {
+ return this.filter ? this.filter(this.results) : this.results
+ }
+ },
+ watch: {
+ term (val) {
+ this.fetchResults(val)
+ }
+ },
+ methods: {
+ fetchResults (term) {
+ clearTimeout(this.timeout)
+ this.timeout = setTimeout(() => {
+ this.results = []
+ if (term) {
+ this.query(term).then((results) => { this.results = results })
+ }
+ }, debounceMilliseconds)
+ },
+ onInputClick () {
+ this.resultsVisible = true
+ },
+ onClickOutside () {
+ this.resultsVisible = false
+ }
+ }
+}
diff --git a/src/components/autosuggest/autosuggest.vue b/src/components/autosuggest/autosuggest.vue
new file mode 100644
index 00000000..91657a2d
--- /dev/null
+++ b/src/components/autosuggest/autosuggest.vue
@@ -0,0 +1,45 @@
+<template>
+ <div class="autosuggest" v-click-outside="onClickOutside">
+ <input v-model="term" :placeholder="placeholder" @click="onInputClick" class="autosuggest-input" />
+ <div class="autosuggest-results" v-if="resultsVisible && filtered.length > 0">
+ <slot v-for="item in filtered" :item="item" />
+ </div>
+ </div>
+</template>
+
+<script src="./autosuggest.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.autosuggest {
+ position: relative;
+
+ &-input {
+ display: block;
+ width: 100%;
+ }
+
+ &-results {
+ position: absolute;
+ left: 0;
+ top: 100%;
+ right: 0;
+ max-height: 400px;
+ background-color: $fallback--lightBg;
+ background-color: var(--lightBg, $fallback--lightBg);
+ border-style: solid;
+ border-width: 1px;
+ border-color: $fallback--border;
+ border-color: var(--border, $fallback--border);
+ border-radius: $fallback--inputRadius;
+ border-radius: var(--inputRadius, $fallback--inputRadius);
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
+ box-shadow: var(--panelShadow);
+ overflow-y: auto;
+ z-index: 1;
+ }
+}
+</style>
diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue
index 8afe8b44..48de6678 100644
--- a/src/components/basic_user_card/basic_user_card.vue
+++ b/src/components/basic_user_card/basic_user_card.vue
@@ -24,19 +24,11 @@
<script src="./basic_user_card.js"></script>
<style lang="scss">
-@import '../../_variables.scss';
-
.basic-user-card {
display: flex;
flex: 1 0;
margin: 0;
- padding-top: 0.6em;
- padding-right: 1em;
- padding-bottom: 0.6em;
- padding-left: 1em;
- border-bottom: 1px solid;
- border-bottom-color: $fallback--border;
- border-bottom-color: var(--border, $fallback--border);
+ padding: 0.6em 1em;
&-collapsed-content {
margin-left: 0.7em;
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue
new file mode 100644
index 00000000..4152b049
--- /dev/null
+++ b/src/components/checkbox/checkbox.vue
@@ -0,0 +1,75 @@
+<template>
+ <label class="checkbox">
+ <input type="checkbox" :checked="checked" @change="$emit('change', $event.target.checked)" :indeterminate.prop="indeterminate">
+ <i class="checkbox-indicator" />
+ <span v-if="!!$slots.default"><slot></slot></span>
+ </label>
+</template>
+
+<script>
+export default {
+ model: {
+ prop: 'checked',
+ event: 'change'
+ },
+ props: ['checked', 'indeterminate']
+}
+</script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.checkbox {
+ position: relative;
+ display: inline-block;
+ padding-left: 1.2em;
+ min-height: 1.2em;
+
+ &-indicator::before {
+ position: absolute;
+ left: 0;
+ top: 0;
+ display: block;
+ content: '✔';
+ transition: color 200ms;
+ width: 1.1em;
+ height: 1.1em;
+ border-radius: $fallback--checkboxRadius;
+ border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
+ box-shadow: 0px 0px 2px black inset;
+ box-shadow: var(--inputShadow);
+ background-color: $fallback--fg;
+ background-color: var(--input, $fallback--fg);
+ vertical-align: top;
+ text-align: center;
+ line-height: 1.1em;
+ font-size: 1.1em;
+ color: transparent;
+ overflow: hidden;
+ box-sizing: border-box;
+ }
+
+ input[type=checkbox] {
+ display: none;
+
+ &:checked + .checkbox-indicator::before {
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ }
+
+ &:indeterminate + .checkbox-indicator::before {
+ content: '–';
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ }
+
+ &:disabled + .checkbox-indicator::before {
+ opacity: .5;
+ }
+ }
+
+ & > span {
+ margin-left: .5em;
+ }
+}
+</style>
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 69058bf6..30600f73 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -1,5 +1,4 @@
-import { reduce, filter, findIndex } from 'lodash'
-import { set } from 'vue'
+import { reduce, filter, findIndex, clone } from 'lodash'
import Status from '../status/status.vue'
const sortById = (a, b) => {
@@ -36,8 +35,7 @@ const conversation = {
data () {
return {
highlight: null,
- expanded: false,
- converationStatusIds: []
+ expanded: false
}
},
props: [
@@ -54,15 +52,6 @@ const conversation = {
status () {
return this.statusoid
},
- idsToShow () {
- if (this.converationStatusIds.length > 0) {
- return this.converationStatusIds
- } else if (this.statusId) {
- return [this.statusId]
- } else {
- return []
- }
- },
statusId () {
if (this.statusoid.retweeted_status) {
return this.statusoid.retweeted_status.id
@@ -70,6 +59,13 @@ const conversation = {
return this.statusoid.id
}
},
+ conversationId () {
+ if (this.statusoid.retweeted_status) {
+ return this.statusoid.retweeted_status.statusnet_conversation_id
+ } else {
+ return this.statusoid.statusnet_conversation_id
+ }
+ },
conversation () {
if (!this.status) {
return []
@@ -79,12 +75,7 @@ const conversation = {
return [this.status]
}
- const statusesObject = this.$store.state.statuses.allStatusesObject
- const conversation = this.idsToShow.reduce((acc, id) => {
- acc.push(statusesObject[id])
- return acc
- }, [])
-
+ const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId])
const statusIndex = findIndex(conversation, { id: this.statusId })
if (statusIndex !== -1) {
conversation[statusIndex] = this.status
@@ -131,10 +122,6 @@ const conversation = {
.then(({ancestors, descendants}) => {
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
this.$store.dispatch('addNewStatuses', { statuses: descendants })
- set(this, 'converationStatusIds', [].concat(
- ancestors.map(_ => _.id).filter(_ => _ !== this.statusId),
- this.statusId,
- descendants.map(_ => _.id).filter(_ => _ !== this.statusId)))
})
.then(() => this.setHighlight(this.statusId))
} else {
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index c39a3ed9..c3bbb597 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -13,7 +13,7 @@
:key="status.id"
:inlineExpanded="collapsable"
:statusoid="status"
- :expandable='!expanded'
+ :expandable='!isExpanded'
:focused="focused(status.id)"
:inConversation="isExpanded"
:highlight="getHighlight()"
diff --git a/src/components/delete_button/delete_button.js b/src/components/delete_button/delete_button.js
index f2920666..22f24625 100644
--- a/src/components/delete_button/delete_button.js
+++ b/src/components/delete_button/delete_button.js
@@ -10,7 +10,11 @@ const DeleteButton = {
},
computed: {
currentUser () { return this.$store.state.users.currentUser },
- canDelete () { return this.currentUser && this.currentUser.rights.delete_others_notice || this.status.user.id === this.currentUser.id }
+ canDelete () {
+ if (!this.currentUser) { return }
+ const superuser = this.currentUser.rights.moderator || this.currentUser.rights.admin
+ return superuser || this.status.user.id === this.currentUser.id
+ }
}
}
diff --git a/src/components/dialog_modal/dialog_modal.js b/src/components/dialog_modal/dialog_modal.js
new file mode 100644
index 00000000..f14e3fe9
--- /dev/null
+++ b/src/components/dialog_modal/dialog_modal.js
@@ -0,0 +1,14 @@
+const DialogModal = {
+ props: {
+ darkOverlay: {
+ default: true,
+ type: Boolean
+ },
+ onCancel: {
+ default: () => {},
+ type: Function
+ }
+ }
+}
+
+export default DialogModal
diff --git a/src/components/dialog_modal/dialog_modal.vue b/src/components/dialog_modal/dialog_modal.vue
new file mode 100644
index 00000000..7621fb20
--- /dev/null
+++ b/src/components/dialog_modal/dialog_modal.vue
@@ -0,0 +1,92 @@
+<template>
+ <span v-bind:class="{ 'dark-overlay': darkOverlay }" @click.self.stop='onCancel()'>
+ <div class="dialog-modal panel panel-default" @click.stop=''>
+ <div class="panel-heading dialog-modal-heading">
+ <div class="title">
+ <slot name="header"></slot>
+ </div>
+ </div>
+ <div class="dialog-modal-content">
+ <slot name="default"></slot>
+ </div>
+ <div class="dialog-modal-footer user-interactions panel-footer">
+ <slot name="footer"></slot>
+ </div>
+ </div>
+ </span>
+</template>
+
+<script src="./dialog_modal.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+// TODO: unify with other modals.
+.dark-overlay {
+ &::before {
+ bottom: 0;
+ content: " ";
+ display: block;
+ cursor: default;
+ left: 0;
+ position: fixed;
+ right: 0;
+ top: 0;
+ background: rgba(27,31,35,.5);
+ z-index: 99;
+ }
+}
+
+.dialog-modal.panel {
+ top: 0;
+ left: 50%;
+ max-height: 80vh;
+ max-width: 90vw;
+ margin: 15vh auto;
+ position: fixed;
+ transform: translateX(-50%);
+ z-index: 999;
+ cursor: default;
+ display: block;
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
+
+ .dialog-modal-heading {
+ padding: .5em .5em;
+ margin-right: auto;
+ margin-bottom: 0;
+ white-space: nowrap;
+ color: var(--panelText);
+ background-color: $fallback--fg;
+ background-color: var(--panel, $fallback--fg);
+
+ .title {
+ margin-bottom: 0;
+ }
+ }
+
+ .dialog-modal-content {
+ margin: 0;
+ padding: 1rem 1rem;
+ background-color: $fallback--lightBg;
+ background-color: var(--lightBg, $fallback--lightBg);
+ white-space: normal;
+ }
+
+ .dialog-modal-footer {
+ margin: 0;
+ padding: .5em .5em;
+ background-color: $fallback--lightBg;
+ background-color: var(--lightBg, $fallback--lightBg);
+ border-top: 1px solid $fallback--bg;
+ border-top: 1px solid var(--bg, $fallback--bg);
+ justify-content: flex-end;
+
+ button {
+ width: auto;
+ margin-left: .5rem;
+ }
+ }
+}
+
+</style>
diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js
index ac4e265a..dc4a0d41 100644
--- a/src/components/follow_card/follow_card.js
+++ b/src/components/follow_card/follow_card.js
@@ -10,8 +10,7 @@ const FollowCard = {
data () {
return {
inProgress: false,
- requestSent: false,
- updated: false
+ requestSent: false
}
},
components: {
@@ -19,10 +18,8 @@ const FollowCard = {
RemoteFollow
},
computed: {
- isMe () { return this.$store.state.users.currentUser.id === this.user.id },
- following () { return this.updated ? this.updated.following : this.user.following },
- showFollow () {
- return !this.following || this.updated && !this.updated.following
+ isMe () {
+ return this.$store.state.users.currentUser.id === this.user.id
},
loggedIn () {
return this.$store.state.users.currentUser
@@ -31,17 +28,15 @@ const FollowCard = {
methods: {
followUser () {
this.inProgress = true
- requestFollow(this.user, this.$store).then(({ sent, updated }) => {
+ requestFollow(this.user, this.$store).then(({ sent }) => {
this.inProgress = false
this.requestSent = sent
- this.updated = updated
})
},
unfollowUser () {
this.inProgress = true
- requestUnfollow(this.user, this.$store).then(({ updated }) => {
+ requestUnfollow(this.user, this.$store).then(() => {
this.inProgress = false
- this.updated = updated
})
}
}
diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue
index 9f314fd3..94e2836f 100644
--- a/src/components/follow_card/follow_card.vue
+++ b/src/components/follow_card/follow_card.vue
@@ -4,34 +4,38 @@
<span class="faint" v-if="!noFollowsYou && user.follows_you">
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span>
- <div class="follow-card-follow-button" v-if="showFollow && !loggedIn">
- <RemoteFollow :user="user" />
- </div>
- <button
- v-if="showFollow && loggedIn"
- class="btn btn-default follow-card-follow-button"
- @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 follow-card-follow-button 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>
+ <template v-if="!loggedIn">
+ <div class="follow-card-follow-button" v-if="!user.following">
+ <RemoteFollow :user="user" />
+ </div>
+ </template>
+ <template v-else>
+ <button
+ v-if="!user.following"
+ class="btn btn-default follow-card-follow-button"
+ @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-else class="btn btn-default follow-card-follow-button 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>
+ </template>
</div>
</basic-user-card>
</template>
diff --git a/src/components/follow_requests/follow_requests.vue b/src/components/follow_requests/follow_requests.vue
index b83c2d68..36901fb4 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">
- <FollowRequestCard v-for="request in requests" :key="request.id" :user="request"/>
+ <FollowRequestCard v-for="request in requests" :key="request.id" :user="request" class="list-item"/>
</div>
</div>
</template>
diff --git a/src/components/list/list.vue b/src/components/list/list.vue
new file mode 100644
index 00000000..7136915b
--- /dev/null
+++ b/src/components/list/list.vue
@@ -0,0 +1,42 @@
+<template>
+ <div class="list">
+ <div v-for="item in items" class="list-item" :key="getKey(item)">
+ <slot name="item" :item="item" />
+ </div>
+ <div class="list-empty-content faint" v-if="items.length === 0 && !!$slots.empty">
+ <slot name="empty" />
+ </div>
+ </div>
+</template>
+
+<script>
+export default {
+ props: {
+ items: {
+ type: Array,
+ default: () => []
+ },
+ getKey: {
+ type: Function,
+ default: item => item.id
+ }
+ }
+}
+</script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.list {
+ &-item:not(:last-child) {
+ border-bottom: 1px solid;
+ border-bottom-color: $fallback--border;
+ border-bottom-color: var(--border, $fallback--border);
+ }
+
+ &-empty-content {
+ text-align: center;
+ padding: 10px;
+ }
+}
+</style>
diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index fb6dc651..dc917e47 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -31,15 +31,19 @@ const LoginForm = {
username: this.user.username,
password: this.user.password
}
- ).then((result) => {
+ ).then(async (result) => {
if (result.error) {
this.authError = result.error
this.user.password = ''
return
}
this.$store.commit('setToken', result.access_token)
- this.$store.dispatch('loginUser', result.access_token)
- this.$router.push({name: 'friends'})
+ try {
+ await this.$store.dispatch('loginUser', result.access_token)
+ this.$router.push({name: 'friends'})
+ } catch (e) {
+ console.log(e)
+ }
})
})
},
diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js
new file mode 100644
index 00000000..3eedeaa1
--- /dev/null
+++ b/src/components/moderation_tools/moderation_tools.js
@@ -0,0 +1,106 @@
+import DialogModal from '../dialog_modal/dialog_modal.vue'
+import Popper from 'vue-popperjs/src/component/popper.js.vue'
+
+const FORCE_NSFW = 'mrf_tag:media-force-nsfw'
+const STRIP_MEDIA = 'mrf_tag:media-strip'
+const FORCE_UNLISTED = 'mrf_tag:force-unlisted'
+const DISABLE_REMOTE_SUBSCRIPTION = 'mrf_tag:disable-remote-subscription'
+const DISABLE_ANY_SUBSCRIPTION = 'mrf_tag:disable-any-subscription'
+const SANDBOX = 'mrf_tag:sandbox'
+const QUARANTINE = 'mrf_tag:quarantine'
+
+const ModerationTools = {
+ props: [
+ 'user'
+ ],
+ data () {
+ return {
+ showDropDown: false,
+ tags: {
+ FORCE_NSFW,
+ STRIP_MEDIA,
+ FORCE_UNLISTED,
+ DISABLE_REMOTE_SUBSCRIPTION,
+ DISABLE_ANY_SUBSCRIPTION,
+ SANDBOX,
+ QUARANTINE
+ },
+ showDeleteUserDialog: false
+ }
+ },
+ components: {
+ DialogModal,
+ Popper
+ },
+ computed: {
+ tagsSet () {
+ return new Set(this.user.tags)
+ },
+ hasTagPolicy () {
+ return this.$store.state.instance.tagPolicyAvailable
+ }
+ },
+ methods: {
+ toggleMenu () {
+ this.showDropDown = !this.showDropDown
+ },
+ hasTag (tagName) {
+ return this.tagsSet.has(tagName)
+ },
+ toggleTag (tag) {
+ const store = this.$store
+ if (this.tagsSet.has(tag)) {
+ store.state.api.backendInteractor.untagUser(this.user, tag).then(response => {
+ if (!response.ok) { return }
+ store.commit('untagUser', {user: this.user, tag})
+ })
+ } else {
+ store.state.api.backendInteractor.tagUser(this.user, tag).then(response => {
+ if (!response.ok) { return }
+ store.commit('tagUser', {user: this.user, tag})
+ })
+ }
+ },
+ toggleRight (right) {
+ const store = this.$store
+ if (this.user.rights[right]) {
+ store.state.api.backendInteractor.deleteRight(this.user, right).then(response => {
+ if (!response.ok) { return }
+ store.commit('updateRight', {user: this.user, right: right, value: false})
+ })
+ } else {
+ store.state.api.backendInteractor.addRight(this.user, right).then(response => {
+ if (!response.ok) { return }
+ store.commit('updateRight', {user: this.user, right: right, value: true})
+ })
+ }
+ },
+ toggleActivationStatus () {
+ const store = this.$store
+ const status = !!this.user.deactivated
+ store.state.api.backendInteractor.setActivationStatus(this.user, status).then(response => {
+ if (!response.ok) { return }
+ store.commit('updateActivationStatus', {user: this.user, status: status})
+ })
+ },
+ deleteUserDialog (show) {
+ this.showDeleteUserDialog = show
+ },
+ deleteUser () {
+ const store = this.$store
+ const user = this.user
+ const {id, name} = user
+ store.state.api.backendInteractor.deleteUser(user)
+ .then(e => {
+ this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id)
+ const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'
+ const isTargetUser = this.$route.params.name === name || this.$route.params.id === id
+ if (isProfile && isTargetUser) {
+ window.history.back()
+ }
+ })
+ }
+ }
+}
+
+export default ModerationTools
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
new file mode 100644
index 00000000..c24a2280
--- /dev/null
+++ b/src/components/moderation_tools/moderation_tools.vue
@@ -0,0 +1,158 @@
+<template>
+<div class='block' style='position: relative'>
+ <Popper
+ trigger="click"
+ @hide='showDropDown = false'
+ append-to-body
+ :options="{
+ placement: 'bottom-end',
+ modifiers: {
+ arrow: { enabled: true },
+ offset: { offset: '0, 5px' },
+ }
+ }">
+ <div class="popper-wrapper">
+ <div class="dropdown-menu">
+ <span v-if='user.is_local'>
+ <button class="dropdown-item" @click='toggleRight("admin")'>
+ {{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
+ </button>
+ <button class="dropdown-item" @click='toggleRight("moderator")'>
+ {{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
+ </button>
+ <div role="separator" class="dropdown-divider"></div>
+ </span>
+ <button class="dropdown-item" @click='toggleActivationStatus()'>
+ {{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
+ </button>
+ <button class="dropdown-item" @click='deleteUserDialog(true)'>
+ {{ $t('user_card.admin_menu.delete_account') }}
+ </button>
+ <div role="separator" class="dropdown-divider" v-if='hasTagPolicy'></div>
+ <span v-if='hasTagPolicy'>
+ <button class="dropdown-item" @click='toggleTag(tags.FORCE_NSFW)'>
+ {{ $t('user_card.admin_menu.force_nsfw') }}
+ <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"></span>
+ </button>
+ <button class="dropdown-item" @click='toggleTag(tags.STRIP_MEDIA)'>
+ {{ $t('user_card.admin_menu.strip_media') }}
+ <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"></span>
+ </button>
+ <button class="dropdown-item" @click='toggleTag(tags.FORCE_UNLISTED)'>
+ {{ $t('user_card.admin_menu.force_unlisted') }}
+ <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"></span>
+ </button>
+ <button class="dropdown-item" @click='toggleTag(tags.SANDBOX)'>
+ {{ $t('user_card.admin_menu.sandbox') }}
+ <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"></span>
+ </button>
+ <button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)'>
+ {{ $t('user_card.admin_menu.disable_remote_subscription') }}
+ <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"></span>
+ </button>
+ <button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)'>
+ {{ $t('user_card.admin_menu.disable_any_subscription') }}
+ <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"></span>
+ </button>
+ <button class="dropdown-item" v-if='user.is_local' @click='toggleTag(tags.QUARANTINE)'>
+ {{ $t('user_card.admin_menu.quarantine') }}
+ <span class="menu-checkbox" v-bind:class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"></span>
+ </button>
+ </span>
+ </div>
+ </div>
+ <button slot="reference" v-bind:class="{ pressed: showDropDown }" @click='toggleMenu'>
+ {{ $t('user_card.admin_menu.moderation') }}
+ </button>
+ </Popper>
+ <DialogModal v-if="showDeleteUserDialog" :onCancel='deleteUserDialog.bind(this, false)'>
+ <span slot="header">{{ $t('user_card.admin_menu.delete_user') }}</span>
+ <p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
+ <span slot="footer">
+ <button @click='deleteUserDialog(false)'>
+ {{ $t('general.cancel') }}
+ </button>
+ <button class="danger" @click='deleteUser()'>
+ {{ $t('user_card.admin_menu.delete_user') }}
+ </button>
+ </span>
+ </DialogModal>
+</div>
+</template>
+
+<script src="./moderation_tools.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+@import '../popper/popper.scss';
+
+.dropdown-menu {
+ display: block;
+ padding: .5rem 0;
+ font-size: 1rem;
+ text-align: left;
+ list-style: none;
+ max-width: 100vw;
+ z-index: 10;
+ box-shadow: 1px 1px 4px rgba(0,0,0,.6);
+ box-shadow: var(--panelShadow);
+ border: none;
+ border-radius: $fallback--btnRadius;
+ border-radius: var(--btnRadius, $fallback--btnRadius);
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
+
+ .dropdown-divider {
+ height: 0;
+ margin: .5rem 0;
+ overflow: hidden;
+ border-top: 1px solid $fallback--border;
+ border-top: 1px solid var(--border, $fallback--border);
+ }
+
+ .dropdown-item {
+ line-height: 21px;
+ margin-right: 5px;
+ overflow: auto;
+ display: block;
+ padding: .25rem 1.0rem .25rem 1.5rem;
+ clear: both;
+ font-weight: 400;
+ text-align: inherit;
+ white-space: normal;
+ border: none;
+ border-radius: 0px;
+ background-color: transparent;
+ box-shadow: none;
+ width: 100%;
+ height: 100%;
+
+ &:hover {
+ // TODO: improve the look on breeze themes
+ background-color: $fallback--fg;
+ background-color: var(--btn, $fallback--fg);
+ box-shadow: none;
+ }
+ }
+}
+
+.menu-checkbox {
+ float: right;
+ min-width: 22px;
+ max-width: 22px;
+ min-height: 22px;
+ max-height: 22px;
+ line-height: 22px;
+ text-align: center;
+ border-radius: 0px;
+ background-color: $fallback--fg;
+ background-color: var(--input, $fallback--fg);
+ box-shadow: 0px 0px 2px black inset;
+ box-shadow: var(--inputShadow);
+
+ &.menu-checkbox-checked::after {
+ content: '✔';
+ }
+}
+
+</style>
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 42a48f3f..e59e7497 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -21,25 +21,28 @@ const Notification = {
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
+ },
+ getUser (notification) {
+ return this.$store.state.users.usersObject[notification.from_profile.id]
}
},
computed: {
userClass () {
- return highlightClass(this.notification.action.user)
+ return highlightClass(this.notification.from_profile)
},
userStyle () {
const highlight = this.$store.state.config.highlight
- const user = this.notification.action.user
+ const user = this.notification.from_profile
return highlightStyle(highlight[user.screen_name])
},
userInStore () {
- return this.$store.getters.findUser(this.notification.action.user.id)
+ return this.$store.getters.findUser(this.notification.from_profile.id)
},
user () {
if (this.userInStore) {
return this.userInStore
}
- return {}
+ return this.notification.from_profile
}
}
}
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 8f532747..ae11d692 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -1,15 +1,20 @@
<template>
- <status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
+ <status
+ v-if="notification.type === 'mention'"
+ :compact="true"
+ :statusoid="notification.status"
+ >
+ </status>
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]" v-else>
- <a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
- <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
+ <a class='avatar-container' :href="notification.from_profile.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
+ <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.from_profile.profile_image_url_original" />
</a>
<div class='notification-right'>
- <UserCard :user="user" :rounded="true" :bordered="true" v-if="userExpanded"/>
+ <UserCard :user="getUser(notification)" :rounded="true" :bordered="true" v-if="userExpanded" />
<span class="notification-details">
<div class="name-and-action">
- <span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
- <span class="username" v-else :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
+ <span class="username" v-if="!!notification.from_profile.name_html" :title="'@'+notification.from_profile.screen_name" v-html="notification.from_profile.name_html"></span>
+ <span class="username" v-else :title="'@'+notification.from_profile.screen_name">{{ notification.from_profile.name }}</span>
<span v-if="notification.type === 'like'">
<i class="fa icon-star lit"></i>
<small>{{$t('notifications.favorited_you')}}</small>
@@ -23,19 +28,24 @@
<small>{{$t('notifications.followed_you')}}</small>
</span>
</div>
- <div class="timeago">
+ <div class="timeago" v-if="notification.type === 'follow'">
+ <span class="faint">
+ <timeago :since="notification.created_at" :auto-update="240"></timeago>
+ </span>
+ </div>
+ <div class="timeago" v-else>
<router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
- <timeago :since="notification.action.created_at" :auto-update="240"></timeago>
+ <timeago :since="notification.created_at" :auto-update="240"></timeago>
</router-link>
</div>
</span>
<div class="follow-text" v-if="notification.type === 'follow'">
- <router-link :to="userProfileLink(notification.action.user)">
- @{{notification.action.user.screen_name}}
+ <router-link :to="userProfileLink(notification.from_profile)">
+ @{{notification.from_profile.screen_name}}
</router-link>
</div>
<template v-else>
- <status class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
+ <status class="faint" :compact="true" :statusoid="notification.action" :noHeading="true"></status>
</template>
</div>
</div>
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index d3db4b29..e341212e 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -10,13 +10,6 @@ const Notifications = {
props: [
'noHeading'
],
- created () {
- const store = this.$store
- const credentials = store.state.users.currentUser.credentials
-
- const fetcherId = notificationsFetcher.startFetching({ store, credentials })
- this.$store.commit('setNotificationFetcher', { fetcherId })
- },
data () {
return {
bottomedOut: false
@@ -56,7 +49,7 @@ const Notifications = {
},
methods: {
markAsSeen () {
- this.$store.dispatch('markNotificationsAsSeen', this.visibleNotifications)
+ this.$store.dispatch('markNotificationsAsSeen')
},
fetchOlderNotifications () {
const store = this.$store
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 634a03ac..88775be1 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -12,7 +12,7 @@
<button v-if="unseenCount" @click.prevent="markAsSeen" class="read-button">{{$t('notifications.read')}}</button>
</div>
<div class="panel-body">
- <div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'>
+ <div v-for="notification in visibleNotifications" :key="notification.id" class="notification" :class='{"unseen": !notification.seen}'>
<div class="notification-overlay"></div>
<notification :notification="notification"></notification>
</div>
diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss
new file mode 100644
index 00000000..0c30d625
--- /dev/null
+++ b/src/components/popper/popper.scss
@@ -0,0 +1,70 @@
+@import '../../_variables.scss';
+
+.popper-wrapper {
+ z-index: 8;
+}
+
+.popper-wrapper .popper__arrow {
+ width: 0;
+ height: 0;
+ border-style: solid;
+ position: absolute;
+ margin: 5px;
+}
+
+.popper-wrapper[x-placement^="top"] {
+ margin-bottom: 5px;
+}
+
+.popper-wrapper[x-placement^="top"] .popper__arrow {
+ border-width: 5px 5px 0 5px;
+ border-color: $fallback--bg transparent transparent transparent;
+ border-color: var(--bg, $fallback--bg) transparent transparent transparent;
+ bottom: -5px;
+ left: calc(50% - 5px);
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.popper-wrapper[x-placement^="bottom"] {
+ margin-top: 5px;
+}
+
+.popper-wrapper[x-placement^="bottom"] .popper__arrow {
+ border-width: 0 5px 5px 5px;
+ border-color: transparent transparent $fallback--bg transparent;
+ border-color: transparent transparent var(--bg, $fallback--bg) transparent;
+ top: -5px;
+ left: calc(50% - 5px);
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+.popper-wrapper[x-placement^="right"] {
+ margin-left: 5px;
+}
+
+.popper-wrapper[x-placement^="right"] .popper__arrow {
+ border-width: 5px 5px 5px 0;
+ border-color: transparent $fallback--bg transparent transparent;
+ border-color: transparent var(--bg, $fallback--bg) transparent transparent;
+ left: -5px;
+ top: calc(50% - 5px);
+ margin-left: 0;
+ margin-right: 0;
+}
+
+.popper-wrapper[x-placement^="left"] {
+ margin-right: 5px;
+}
+
+.popper-wrapper[x-placement^="left"] .popper__arrow {
+ border-width: 5px 0 5px 5px;
+ border-color: transparent transparent transparent $fallback--bg;
+ border-color: transparent transparent transparent var(--bg, $fallback--bg);
+ right: -5px;
+ top: calc(50% - 5px);
+ margin-left: 0;
+ margin-right: 0;
+}
+
diff --git a/src/components/progress_button/progress_button.vue b/src/components/progress_button/progress_button.vue
new file mode 100644
index 00000000..737360bb
--- /dev/null
+++ b/src/components/progress_button/progress_button.vue
@@ -0,0 +1,35 @@
+<template>
+ <button :disabled="progress || disabled" @click="onClick">
+ <template v-if="progress">
+ <slot name="progress" />
+ </template>
+ <template v-else>
+ <slot />
+ </template>
+ </button>
+</template>
+
+<script>
+export default {
+ props: {
+ disabled: {
+ type: Boolean
+ },
+ click: { // click event handler. Must return a promise
+ type: Function,
+ default: () => Promise.resolve()
+ }
+ },
+ data () {
+ return {
+ progress: false
+ }
+ },
+ methods: {
+ onClick () {
+ this.progress = true
+ this.click().then(() => { this.progress = false })
+ }
+ }
+}
+</script>
diff --git a/src/components/public_and_external_timeline/public_and_external_timeline.js b/src/components/public_and_external_timeline/public_and_external_timeline.js
index d45677e0..f614c13b 100644
--- a/src/components/public_and_external_timeline/public_and_external_timeline.js
+++ b/src/components/public_and_external_timeline/public_and_external_timeline.js
@@ -7,7 +7,7 @@ const PublicAndExternalTimeline = {
timeline () { return this.$store.state.statuses.timelines.publicAndExternal }
},
created () {
- this.$store.dispatch('startFetching', { timeline: 'publicAndExternal' })
+ this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' })
},
destroyed () {
this.$store.dispatch('stopFetching', 'publicAndExternal')
diff --git a/src/components/public_timeline/public_timeline.js b/src/components/public_timeline/public_timeline.js
index 64c951ac..8976a99c 100644
--- a/src/components/public_timeline/public_timeline.js
+++ b/src/components/public_timeline/public_timeline.js
@@ -7,7 +7,7 @@ const PublicTimeline = {
timeline () { return this.$store.state.statuses.timelines.public }
},
created () {
- this.$store.dispatch('startFetching', { timeline: 'public' })
+ this.$store.dispatch('startFetchingTimeline', { timeline: 'public' })
},
destroyed () {
this.$store.dispatch('stopFetching', 'public')
diff --git a/src/components/selectable_list/selectable_list.js b/src/components/selectable_list/selectable_list.js
new file mode 100644
index 00000000..10980d46
--- /dev/null
+++ b/src/components/selectable_list/selectable_list.js
@@ -0,0 +1,66 @@
+import List from '../list/list.vue'
+import Checkbox from '../checkbox/checkbox.vue'
+
+const SelectableList = {
+ components: {
+ List,
+ Checkbox
+ },
+ props: {
+ items: {
+ type: Array,
+ default: () => []
+ },
+ getKey: {
+ type: Function,
+ default: item => item.id
+ }
+ },
+ data () {
+ return {
+ selected: []
+ }
+ },
+ computed: {
+ allKeys () {
+ return this.items.map(this.getKey)
+ },
+ filteredSelected () {
+ return this.allKeys.filter(key => this.selected.indexOf(key) !== -1)
+ },
+ allSelected () {
+ return this.filteredSelected.length === this.items.length
+ },
+ noneSelected () {
+ return this.filteredSelected.length === 0
+ },
+ someSelected () {
+ return !this.allSelected && !this.noneSelected
+ }
+ },
+ methods: {
+ isSelected (item) {
+ return this.filteredSelected.indexOf(this.getKey(item)) !== -1
+ },
+ toggle (checked, item) {
+ const key = this.getKey(item)
+ const oldChecked = this.isSelected(key)
+ if (checked !== oldChecked) {
+ if (checked) {
+ this.selected.push(key)
+ } else {
+ this.selected.splice(this.selected.indexOf(key), 1)
+ }
+ }
+ },
+ toggleAll (value) {
+ if (value) {
+ this.selected = this.allKeys.slice(0)
+ } else {
+ this.selected = []
+ }
+ }
+ }
+}
+
+export default SelectableList
diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue
new file mode 100644
index 00000000..ba1e5266
--- /dev/null
+++ b/src/components/selectable_list/selectable_list.vue
@@ -0,0 +1,59 @@
+<template>
+ <div class="selectable-list">
+ <div class="selectable-list-header" v-if="items.length > 0">
+ <div class="selectable-list-checkbox-wrapper">
+ <Checkbox :checked="allSelected" @change="toggleAll" :indeterminate="someSelected">{{ $t('selectable_list.select_all') }}</Checkbox>
+ </div>
+ <div class="selectable-list-header-actions">
+ <slot name="header" :selected="filteredSelected" />
+ </div>
+ </div>
+ <List :items="items" :getKey="getKey">
+ <template slot="item" slot-scope="{item}">
+ <div class="selectable-list-item-inner" :class="{ 'selectable-list-item-selected-inner': isSelected(item) }">
+ <div class="selectable-list-checkbox-wrapper">
+ <Checkbox :checked="isSelected(item)" @change="checked => toggle(checked, item)" />
+ </div>
+ <slot name="item" :item="item" />
+ </div>
+ </template>
+ <template slot="empty"><slot name="empty" /></template>
+ </List>
+ </div>
+</template>
+
+<script src="./selectable_list.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.selectable-list {
+ &-item-inner {
+ display: flex;
+ align-items: center;
+ }
+
+ &-item-selected-inner {
+ background-color: $fallback--lightBg;
+ background-color: var(--lightBg, $fallback--lightBg);
+ }
+
+ &-header {
+ display: flex;
+ align-items: center;
+ padding: 0.6em 0;
+ border-bottom: 2px solid;
+ border-bottom-color: $fallback--border;
+ border-bottom-color: var(--border, $fallback--border);
+
+ &-actions {
+ flex: 1;
+ }
+ }
+
+ &-checkbox-wrapper {
+ padding: 0 10px;
+ flex: none;
+ }
+}
+</style>
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index 6ee103c7..6890220f 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -42,9 +42,7 @@
</li>
<li>
<input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal">
- <label for="collapseMessageWithSubject">
- {{$t('settings.collapse_subject')}} {{$t('settings.instance_default', { value: collapseMessageWithSubjectDefault })}}
- </label>
+ <label for="collapseMessageWithSubject">{{$t('settings.collapse_subject')}} {{$t('settings.instance_default', { value: collapseMessageWithSubjectDefault })}}</label>
</li>
<li>
<input type="checkbox" id="streaming" v-model="streamingLocal">
@@ -330,6 +328,7 @@
textarea {
width: 100%;
+ max-width: 100%;
height: 100px;
}
diff --git a/src/components/tag_timeline/tag_timeline.js b/src/components/tag_timeline/tag_timeline.js
index 41b09706..458eb1c5 100644
--- a/src/components/tag_timeline/tag_timeline.js
+++ b/src/components/tag_timeline/tag_timeline.js
@@ -3,7 +3,7 @@ import Timeline from '../timeline/timeline.vue'
const TagTimeline = {
created () {
this.$store.commit('clearTimeline', { timeline: 'tag' })
- this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag })
+ this.$store.dispatch('startFetchingTimeline', { timeline: 'tag', tag: this.tag })
},
components: {
Timeline
@@ -15,7 +15,7 @@ const TagTimeline = {
watch: {
tag () {
this.$store.commit('clearTimeline', { timeline: 'tag' })
- this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag })
+ this.$store.dispatch('startFetchingTimeline', { timeline: 'tag', tag: this.tag })
}
},
destroyed () {
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 1da7d5cc..19d9a9ac 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -52,7 +52,7 @@ const Timeline = {
window.addEventListener('scroll', this.scrollLoad)
- if (this.timelineName === 'friends' && !credentials) { return false }
+ if (store.state.api.fetchers[this.timelineName]) { return false }
timelineFetcher.fetchAndUpdate({
store,
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 197c61d5..1a100de3 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -1,5 +1,6 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue'
+import ModerationTools from '../moderation_tools/moderation_tools.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@@ -93,15 +94,17 @@ export default {
}
},
visibleRole () {
- const validRole = (this.user.role === 'admin' || this.user.role === 'moderator')
- const showRole = this.isOtherUser || this.user.show_role
-
- return validRole && showRole && this.user.role
+ const rights = this.user.rights
+ if (!rights) { return }
+ const validRole = rights.admin || rights.moderator
+ const roleTitle = rights.admin ? 'admin' : 'moderator'
+ return validRole && roleTitle
}
},
components: {
UserAvatar,
- RemoteFollow
+ RemoteFollow,
+ ModerationTools
},
methods: {
followUser () {
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 3259d1c5..e1d3ff57 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -11,7 +11,7 @@
<div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
<router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser">
- <i class="button-icon icon-pencil usersettings" :title="$t('tool_tip.user_settings')"></i>
+ <i class="button-icon icon-wrench usersettings" :title="$t('tool_tip.user_settings')"></i>
</router-link>
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local">
<i class="icon-link-ext usersettings"></i>
@@ -99,6 +99,8 @@
</button>
</span>
</div>
+ <ModerationTools :user='user' v-if='loggedIn.role === "admin"'>
+ </ModerationTools>
</div>
</div>
</div>
@@ -160,7 +162,7 @@
max-width: 100%;
max-height: 400px;
- .emoji {
+ &.emoji {
width: 32px;
height: 32px;
}
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 1df06fe6..4eddb8b1 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -1,47 +1,37 @@
-import { compose } from 'vue-compose'
import get from 'lodash/get'
import UserCard from '../user_card/user_card.vue'
import FollowCard from '../follow_card/follow_card.vue'
import Timeline from '../timeline/timeline.vue'
+import ModerationTools from '../moderation_tools/moderation_tools.vue'
+import List from '../list/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.findUser(props.userId), 'followers', []),
- destory: (props, $store) => $store.dispatch('clearFollowers', props.userId),
- childPropName: 'entries',
- additionalPropNames: ['userId']
- }),
- withList({ getEntryProps: user => ({ user }) })
-)(FollowCard)
+const FollowerList = withLoadMore({
+ fetch: (props, $store) => $store.dispatch('fetchFollowers', props.userId),
+ select: (props, $store) => get($store.getters.findUser(props.userId), 'followerIds', []).map(id => $store.getters.findUser(id)),
+ destroy: (props, $store) => $store.dispatch('clearFollowers', props.userId),
+ childPropName: 'items',
+ additionalPropNames: ['userId']
+})(List)
-const FriendList = compose(
- withLoadMore({
- fetch: (props, $store) => $store.dispatch('addFriends', props.userId),
- select: (props, $store) => get($store.getters.findUser(props.userId), 'friends', []),
- destory: (props, $store) => $store.dispatch('clearFriends', props.userId),
- childPropName: 'entries',
- additionalPropNames: ['userId']
- }),
- withList({ getEntryProps: user => ({ user }) })
-)(FollowCard)
+const FriendList = withLoadMore({
+ fetch: (props, $store) => $store.dispatch('fetchFriends', props.userId),
+ select: (props, $store) => get($store.getters.findUser(props.userId), 'friendIds', []).map(id => $store.getters.findUser(id)),
+ destroy: (props, $store) => $store.dispatch('clearFriends', props.userId),
+ childPropName: 'items',
+ additionalPropNames: ['userId']
+})(List)
const UserProfile = {
data () {
return {
error: false,
- fetchedUserId: null
+ userId: null
}
},
created () {
- if (!this.user.id) {
- this.fetchUserId()
- .then(() => this.startUp())
- } else {
- this.startUp()
- }
+ const routeParams = this.$route.params
+ this.load(routeParams.name || routeParams.id)
},
destroyed () {
this.cleanUp()
@@ -56,26 +46,12 @@ const UserProfile = {
media () {
return this.$store.state.statuses.timelines.media
},
- userId () {
- return this.$route.params.id || this.user.id || this.fetchedUserId
- },
- userName () {
- return this.$route.params.name || this.user.screen_name
- },
isUs () {
return this.userId && this.$store.state.users.currentUser.id &&
this.userId === this.$store.state.users.currentUser.id
},
- userInStore () {
- const routeParams = this.$route.params
- // This needs fetchedUserId so that computed will be refreshed when user is fetched
- return this.$store.getters.findUser(this.fetchedUserId || routeParams.name || routeParams.id)
- },
user () {
- if (this.userInStore) {
- return this.userInStore
- }
- return {}
+ return this.$store.getters.findUser(this.userId)
},
isExternal () {
return this.$route.name === 'external-user-profile'
@@ -88,39 +64,36 @@ const UserProfile = {
}
},
methods: {
- startFetchFavorites () {
- if (this.isUs) {
- this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.userId })
- }
- },
- fetchUserId () {
- let fetchPromise
- if (this.userId && !this.$route.params.name) {
- fetchPromise = this.$store.dispatch('fetchUser', this.userId)
+ load (userNameOrId) {
+ // Check if user data is already loaded in store
+ const user = this.$store.getters.findUser(userNameOrId)
+ if (user) {
+ this.userId = user.id
+ this.fetchTimelines()
} else {
- fetchPromise = this.$store.dispatch('fetchUser', this.userName)
+ this.$store.dispatch('fetchUser', userNameOrId)
.then(({ id }) => {
- this.fetchedUserId = id
+ this.userId = id
+ this.fetchTimelines()
+ })
+ .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')
+ }
})
}
- return fetchPromise
- .catch((reason) => {
- const errorMessage = get(reason, 'error.error')
- if (errorMessage === 'No user with such user_id') { // Known error
- this.error = this.$t('user_profile.profile_does_not_exist')
- } else if (errorMessage) {
- this.error = errorMessage
- } else {
- this.error = this.$t('user_profile.profile_loading_error')
- }
- })
- .then(() => this.startUp())
},
- startUp () {
- if (this.userId) {
- this.$store.dispatch('startFetching', { timeline: 'user', userId: this.userId })
- this.$store.dispatch('startFetching', { timeline: 'media', userId: this.userId })
- this.startFetchFavorites()
+ fetchTimelines () {
+ const userId = this.userId
+ this.$store.dispatch('startFetchingTimeline', { timeline: 'user', userId })
+ this.$store.dispatch('startFetchingTimeline', { timeline: 'media', userId })
+ if (this.isUs) {
+ this.$store.dispatch('startFetchingTimeline', { timeline: 'favorites', userId })
}
},
cleanUp () {
@@ -133,18 +106,16 @@ const UserProfile = {
}
},
watch: {
- // userId can be undefined if we don't know it yet
- userId (newVal) {
+ '$route.params.id': function (newVal) {
if (newVal) {
this.cleanUp()
- this.startUp()
+ this.load(newVal)
}
},
- userName () {
- if (this.$route.params.name) {
- this.fetchUserId()
+ '$route.params.name': function (newVal) {
+ if (newVal) {
this.cleanUp()
- this.startUp()
+ this.load(newVal)
}
},
$route () {
@@ -155,7 +126,9 @@ const UserProfile = {
UserCard,
Timeline,
FollowerList,
- FriendList
+ FriendList,
+ ModerationTools,
+ FollowCard
}
}
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index d449eb85..71c625b7 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -1,6 +1,6 @@
<template>
<div>
- <div v-if="user.id" class="user-profile panel panel-default">
+ <div v-if="user" class="user-profile panel panel-default">
<UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/>
<tab-switcher :renderOnlyFocused="true" ref="tabSwitcher">
<Timeline
@@ -14,10 +14,18 @@
:user-id="userId"
/>
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
- <FriendList :userId="userId" />
+ <FriendList :userId="userId">
+ <template slot="item" slot-scope="{item}">
+ <FollowCard :user="item" />
+ </template>
+ </FriendList>
</div>
<div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
- <FollowerList :userId="userId" :entryProps="{noFollowsYou: isUs}" />
+ <FollowerList :userId="userId">
+ <template slot="item" slot-scope="{item}">
+ <FollowCard :user="item" :noFollowsYou="isUs" />
+ </template>
+ </FollowerList>
</div>
<Timeline
:label="$t('user_card.media')"
diff --git a/src/components/user_search/user_search.js b/src/components/user_search/user_search.js
index 55040826..62dafdf1 100644
--- a/src/components/user_search/user_search.js
+++ b/src/components/user_search/user_search.js
@@ -1,5 +1,6 @@
import FollowCard from '../follow_card/follow_card.vue'
-import userSearchApi from '../../services/new_api/user_search.js'
+import map from 'lodash/map'
+
const userSearch = {
components: {
FollowCard
@@ -10,10 +11,15 @@ const userSearch = {
data () {
return {
username: '',
- users: [],
+ userIds: [],
loading: false
}
},
+ computed: {
+ users () {
+ return this.userIds.map(userId => this.$store.getters.findUser(userId))
+ }
+ },
mounted () {
this.search(this.query)
},
@@ -33,10 +39,10 @@ const userSearch = {
return
}
this.loading = true
- userSearchApi.search({query, store: this.$store})
+ this.$store.dispatch('searchUsers', query)
.then((res) => {
this.loading = false
- this.users = res
+ this.userIds = map(res, 'id')
})
}
}
diff --git a/src/components/user_search/user_search.vue b/src/components/user_search/user_search.vue
index 1269eea6..890b3c13 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">
- <FollowCard v-for="user in users" :key="user.id" :user="user"/>
+ <FollowCard v-for="user in users" :key="user.id" :user="user" class="list-item"/>
</div>
</div>
</template>
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index b6a0479d..e88ee612 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -1,6 +1,7 @@
-import { compose } from 'vue-compose'
import unescape from 'lodash/unescape'
import get from 'lodash/get'
+import map from 'lodash/map'
+import reject from 'lodash/reject'
import TabSwitcher from '../tab_switcher/tab_switcher.js'
import ImageCropper from '../image_cropper/image_cropper.vue'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
@@ -8,27 +9,24 @@ import ScopeSelector from '../scope_selector/scope_selector.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import BlockCard from '../block_card/block_card.vue'
import MuteCard from '../mute_card/mute_card.vue'
+import SelectableList from '../selectable_list/selectable_list.vue'
+import ProgressButton from '../progress_button/progress_button.vue'
import EmojiInput from '../emoji-input/emoji-input.vue'
+import Autosuggest from '../autosuggest/autosuggest.vue'
import withSubscription from '../../hocs/with_subscription/with_subscription'
-import withList from '../../hocs/with_list/with_list'
+import userSearchApi from '../../services/new_api/user_search.js'
-const BlockList = compose(
- withSubscription({
- fetch: (props, $store) => $store.dispatch('fetchBlocks'),
- select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
- childPropName: 'entries'
- }),
- withList({ getEntryProps: userId => ({ userId }) })
-)(BlockCard)
+const BlockList = withSubscription({
+ fetch: (props, $store) => $store.dispatch('fetchBlocks'),
+ select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
+ childPropName: 'items'
+})(SelectableList)
-const MuteList = compose(
- withSubscription({
- fetch: (props, $store) => $store.dispatch('fetchMutes'),
- select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
- childPropName: 'entries'
- }),
- withList({ getEntryProps: userId => ({ userId }) })
-)(MuteCard)
+const MuteList = withSubscription({
+ fetch: (props, $store) => $store.dispatch('fetchMutes'),
+ select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
+ childPropName: 'items'
+})(SelectableList)
const UserSettings = {
data () {
@@ -73,7 +71,11 @@ const UserSettings = {
ImageCropper,
BlockList,
MuteList,
- EmojiInput
+ EmojiInput,
+ Autosuggest,
+ BlockCard,
+ MuteCard,
+ ProgressButton
},
computed: {
user () {
@@ -334,6 +336,40 @@ const UserSettings = {
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
this.$store.dispatch('revokeToken', id)
}
+ },
+ filterUnblockedUsers (userIds) {
+ return reject(userIds, (userId) => {
+ const user = this.$store.getters.findUser(userId)
+ return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
+ })
+ },
+ filterUnMutedUsers (userIds) {
+ return reject(userIds, (userId) => {
+ const user = this.$store.getters.findUser(userId)
+ return !user || user.muted || user.id === this.$store.state.users.currentUser.id
+ })
+ },
+ queryUserIds (query) {
+ return userSearchApi.search({query, store: this.$store})
+ .then((users) => {
+ this.$store.dispatch('addNewUsers', users)
+ return map(users, 'id')
+ })
+ },
+ blockUsers (ids) {
+ return this.$store.dispatch('blockUsers', ids)
+ },
+ unblockUsers (ids) {
+ return this.$store.dispatch('unblockUsers', ids)
+ },
+ muteUsers (ids) {
+ return this.$store.dispatch('muteUsers', ids)
+ },
+ unmuteUsers (ids) {
+ return this.$store.dispatch('unmuteUsers', ids)
+ },
+ identity (value) {
+ return value
}
}
}
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index c08698dc..d68e68fa 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -22,7 +22,7 @@
<div class="setting-item" >
<h2>{{$t('settings.name_bio')}}</h2>
<p>{{$t('settings.name')}}</p>
- <EmojiInput
+ <EmojiInput
type="text"
v-model="newName"
id="username"
@@ -195,15 +195,51 @@
</div>
<div :label="$t('settings.blocks_tab')">
- <block-list :refresh="true">
+ <div class="profile-edit-usersearch-wrapper">
+ <Autosuggest :filter="filterUnblockedUsers" :query="queryUserIds" :placeholder="$t('settings.search_user_to_block')">
+ <BlockCard slot-scope="row" :userId="row.item"/>
+ </Autosuggest>
+ </div>
+ <BlockList :refresh="true" :getKey="identity">
+ <template slot="header" slot-scope="{selected}">
+ <div class="profile-edit-bulk-actions">
+ <ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => blockUsers(selected)">
+ {{ $t('user_card.block') }}
+ <template slot="progress">{{ $t('user_card.block_progress') }}</template>
+ </ProgressButton>
+ <ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => unblockUsers(selected)">
+ {{ $t('user_card.unblock') }}
+ <template slot="progress">{{ $t('user_card.unblock_progress') }}</template>
+ </ProgressButton>
+ </div>
+ </template>
+ <template slot="item" slot-scope="{item}"><BlockCard :userId="item" /></template>
<template slot="empty">{{$t('settings.no_blocks')}}</template>
- </block-list>
+ </BlockList>
</div>
<div :label="$t('settings.mutes_tab')">
- <mute-list :refresh="true">
+ <div class="profile-edit-usersearch-wrapper">
+ <Autosuggest :filter="filterUnMutedUsers" :query="queryUserIds" :placeholder="$t('settings.search_user_to_mute')">
+ <MuteCard slot-scope="row" :userId="row.item"/>
+ </Autosuggest>
+ </div>
+ <MuteList :refresh="true" :getKey="identity">
+ <template slot="header" slot-scope="{selected}">
+ <div class="profile-edit-bulk-actions">
+ <ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => muteUsers(selected)">
+ {{ $t('user_card.mute') }}
+ <template slot="progress">{{ $t('user_card.mute_progress') }}</template>
+ </ProgressButton>
+ <ProgressButton class="btn btn-default" v-if="selected.length > 0" :click="() => unmuteUsers(selected)">
+ {{ $t('user_card.unmute') }}
+ <template slot="progress">{{ $t('user_card.unmute_progress') }}</template>
+ </ProgressButton>
+ </div>
+ </template>
+ <template slot="item" slot-scope="{item}"><MuteCard :userId="item" /></template>
<template slot="empty">{{$t('settings.no_mutes')}}</template>
- </mute-list>
+ </MuteList>
</div>
</tab-switcher>
</div>
@@ -262,5 +298,19 @@
text-align: right;
}
}
+
+ &-usersearch-wrapper {
+ padding: 1em;
+ }
+
+ &-bulk-actions {
+ text-align: right;
+ padding: 0 1em;
+ min-height: 28px;
+
+ button {
+ width: 10em;
+ }
+ }
}
</style>
diff --git a/src/components/who_to_follow/who_to_follow.vue b/src/components/who_to_follow/who_to_follow.vue
index 1630f5ac..8bc9a728 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">
- <FollowCard v-for="user in users" :key="user.id" :user="user"/>
+ <FollowCard v-for="user in users" :key="user.id" :user="user" class="list-item"/>
</div>
</div>
</template>