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 ...
This commit is contained in:
commit
e0247e21f6
76 changed files with 1859 additions and 513 deletions
|
@ -24,11 +24,13 @@
|
|||
"localforage": "^1.5.0",
|
||||
"object-path": "^0.11.3",
|
||||
"phoenix": "^1.3.0",
|
||||
"popper.js": "^1.14.7",
|
||||
"sanitize-html": "^1.13.0",
|
||||
"v-click-outside": "^2.1.1",
|
||||
"vue": "^2.5.13",
|
||||
"vue-chat-scroll": "^1.2.1",
|
||||
"vue-compose": "^0.7.1",
|
||||
"vue-i18n": "^7.3.2",
|
||||
"vue-popperjs": "^2.0.3",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue-template-compiler": "^2.3.4",
|
||||
"vue-timeago": "^3.1.2",
|
||||
|
|
|
@ -101,6 +101,14 @@ button {
|
|||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg)
|
||||
}
|
||||
|
||||
&.danger {
|
||||
// TODO: add better color variable
|
||||
color: $fallback--text;
|
||||
color: var(--alertErrorPanelText, $fallback--text);
|
||||
background-color: $fallback--alertError;
|
||||
background-color: var(--alertError, $fallback--alertError);
|
||||
}
|
||||
}
|
||||
|
||||
label.select {
|
||||
|
|
|
@ -171,9 +171,10 @@ const getCustomEmoji = async ({ store }) => {
|
|||
try {
|
||||
const res = await window.fetch('/api/pleroma/emoji.json')
|
||||
if (res.ok) {
|
||||
const values = await res.json()
|
||||
const result = await res.json()
|
||||
const values = Array.isArray(result) ? Object.assign({}, ...result) : result
|
||||
const emoji = Object.keys(values).map((key) => {
|
||||
return { shortcode: key, image_url: values[key] }
|
||||
return { shortcode: key, image_url: values[key].image_url || values[key] }
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji })
|
||||
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true })
|
||||
|
@ -211,6 +212,7 @@ const getNodeInfo = async ({ store }) => {
|
|||
|
||||
const frontendVersion = window.___pleromafe_commit_hash
|
||||
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
|
||||
store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
|
||||
} else {
|
||||
throw (res)
|
||||
}
|
||||
|
|
52
src/components/autosuggest/autosuggest.js
Normal file
52
src/components/autosuggest/autosuggest.js
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
45
src/components/autosuggest/autosuggest.vue
Normal file
45
src/components/autosuggest/autosuggest.vue
Normal file
|
@ -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>
|
|
@ -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;
|
||||
|
|
75
src/components/checkbox/checkbox.vue
Normal file
75
src/components/checkbox/checkbox.vue
Normal file
|
@ -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>
|
|
@ -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 {
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
:key="status.id"
|
||||
:inlineExpanded="collapsable"
|
||||
:statusoid="status"
|
||||
:expandable='!expanded'
|
||||
:expandable='!isExpanded'
|
||||
:focused="focused(status.id)"
|
||||
:inConversation="isExpanded"
|
||||
:highlight="getHighlight()"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
14
src/components/dialog_modal/dialog_modal.js
Normal file
14
src/components/dialog_modal/dialog_modal.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
const DialogModal = {
|
||||
props: {
|
||||
darkOverlay: {
|
||||
default: true,
|
||||
type: Boolean
|
||||
},
|
||||
onCancel: {
|
||||
default: () => {},
|
||||
type: Function
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DialogModal
|
92
src/components/dialog_modal/dialog_modal.vue
Normal file
92
src/components/dialog_modal/dialog_modal.vue
Normal file
|
@ -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>
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,14 @@
|
|||
<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">
|
||||
<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="showFollow && loggedIn"
|
||||
v-if="!user.following"
|
||||
class="btn btn-default follow-card-follow-button"
|
||||
@click="followUser"
|
||||
:disabled="inProgress"
|
||||
|
@ -24,7 +27,7 @@
|
|||
{{ $t('user_card.follow') }}
|
||||
</template>
|
||||
</button>
|
||||
<button v-if="following" class="btn btn-default follow-card-follow-button pressed" @click="unfollowUser" :disabled="inProgress">
|
||||
<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>
|
||||
|
@ -32,6 +35,7 @@
|
|||
{{ $t('user_card.follow_unfollow') }}
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</basic-user-card>
|
||||
</template>
|
||||
|
|
|
@ -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>
|
||||
|
|
42
src/components/list/list.vue
Normal file
42
src/components/list/list.vue
Normal file
|
@ -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>
|
|
@ -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)
|
||||
try {
|
||||
await this.$store.dispatch('loginUser', result.access_token)
|
||||
this.$router.push({name: 'friends'})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
|
106
src/components/moderation_tools/moderation_tools.js
Normal file
106
src/components/moderation_tools/moderation_tools.js
Normal file
|
@ -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
|
158
src/components/moderation_tools/moderation_tools.vue
Normal file
158
src/components/moderation_tools/moderation_tools.vue
Normal file
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
70
src/components/popper/popper.scss
Normal file
70
src/components/popper/popper.scss
Normal file
|
@ -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;
|
||||
}
|
||||
|
35
src/components/progress_button/progress_button.vue
Normal file
35
src/components/progress_button/progress_button.vue
Normal file
|
@ -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>
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
66
src/components/selectable_list/selectable_list.js
Normal file
66
src/components/selectable_list/selectable_list.js
Normal file
|
@ -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
|
59
src/components/selectable_list/selectable_list.vue
Normal file
59
src/components/selectable_list/selectable_list.vue
Normal file
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
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']
|
||||
}),
|
||||
withList({ getEntryProps: user => ({ user }) })
|
||||
)(FollowCard)
|
||||
})(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',
|
||||
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']
|
||||
}),
|
||||
withList({ getEntryProps: user => ({ user }) })
|
||||
)(FollowCard)
|
||||
})(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,22 +64,18 @@ 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()
|
||||
})
|
||||
}
|
||||
return fetchPromise
|
||||
.catch((reason) => {
|
||||
const errorMessage = get(reason, 'error.error')
|
||||
if (errorMessage === 'No user with such user_id') { // Known error
|
||||
|
@ -114,13 +86,14 @@ const UserProfile = {
|
|||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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')"
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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({
|
||||
const BlockList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
|
||||
childPropName: 'entries'
|
||||
}),
|
||||
withList({ getEntryProps: userId => ({ userId }) })
|
||||
)(BlockCard)
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const MuteList = compose(
|
||||
withSubscription({
|
||||
const MuteList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchMutes'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
|
||||
childPropName: 'entries'
|
||||
}),
|
||||
withList({ getEntryProps: userId => ({ userId }) })
|
||||
)(MuteCard)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
import Vue from 'vue'
|
||||
import map from 'lodash/map'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import './with_list.scss'
|
||||
|
||||
const defaultEntryPropsGetter = entry => ({ entry })
|
||||
const defaultKeyGetter = entry => entry.id
|
||||
|
||||
const withList = ({
|
||||
getEntryProps = defaultEntryPropsGetter, // function to accept entry and index values and return props to be passed into the item component
|
||||
getKey = defaultKeyGetter // funciton to accept entry and index values and return key prop value
|
||||
}) => (ItemComponent) => (
|
||||
Vue.component('withList', {
|
||||
props: [
|
||||
'entries', // array of entry
|
||||
'entryProps', // additional props to be passed into each entry
|
||||
'entryListeners' // additional event listeners to be passed into each entry
|
||||
],
|
||||
render (createElement) {
|
||||
return (
|
||||
<div class="with-list">
|
||||
{map(this.entries, (entry, index) => {
|
||||
const props = {
|
||||
key: getKey(entry, index),
|
||||
props: {
|
||||
...this.$props.entryProps,
|
||||
...getEntryProps(entry, index)
|
||||
},
|
||||
on: this.$props.entryListeners
|
||||
}
|
||||
return <ItemComponent {...props} />
|
||||
})}
|
||||
{isEmpty(this.entries) && this.$slots.empty && <div class="with-list-empty-content faint">{this.$slots.empty}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
export default withList
|
|
@ -1,6 +0,0 @@
|
|||
.with-list {
|
||||
&-empty-content {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,13 @@
|
|||
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.with-load-more {
|
||||
&-footer {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border-top: 1px solid;
|
||||
border-top-color: $fallback--border;
|
||||
border-top-color: var(--border, $fallback--border);
|
||||
|
||||
.error {
|
||||
font-size: 14px;
|
||||
|
|
|
@ -73,7 +73,8 @@
|
|||
"content_type": {
|
||||
"text/plain": "Prostý text",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown"
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "Předmět (volitelný)",
|
||||
"default": "Právě jsem přistál v L.A.",
|
||||
|
|
|
@ -22,7 +22,8 @@
|
|||
"generic_error": "An error occured",
|
||||
"optional": "optional",
|
||||
"show_more": "Show more",
|
||||
"show_less": "Show less"
|
||||
"show_less": "Show less",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Crop picture",
|
||||
|
@ -76,7 +77,8 @@
|
|||
"content_type": {
|
||||
"text/plain": "Plain text",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown"
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "Subject (optional)",
|
||||
"default": "Just landed in L.A.",
|
||||
|
@ -111,6 +113,9 @@
|
|||
"password_confirmation_match": "should be the same as password"
|
||||
}
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "Select all"
|
||||
},
|
||||
"settings": {
|
||||
"app_name": "App name",
|
||||
"attachmentRadius": "Attachments",
|
||||
|
@ -216,6 +221,8 @@
|
|||
"reply_visibility_self": "Only show replies directed at me",
|
||||
"saving_err": "Error saving settings",
|
||||
"saving_ok": "Settings saved",
|
||||
"search_user_to_block": "Search whom you want to block",
|
||||
"search_user_to_mute": "Search whom you want to mute",
|
||||
"security_tab": "Security",
|
||||
"scope_copy": "Copy scope when replying (DMs are always copied)",
|
||||
"minimal_scopes_mode": "Minimize post scope selection options",
|
||||
|
@ -403,7 +410,26 @@
|
|||
"block_progress": "Blocking...",
|
||||
"unmute": "Unmute",
|
||||
"unmute_progress": "Unmuting...",
|
||||
"mute_progress": "Muting..."
|
||||
"mute_progress": "Muting...",
|
||||
"admin_menu": {
|
||||
"moderation": "Moderation",
|
||||
"grant_admin": "Grant Admin",
|
||||
"revoke_admin": "Revoke Admin",
|
||||
"grant_moderator": "Grant Moderator",
|
||||
"revoke_moderator": "Revoke Moderator",
|
||||
"activate_account": "Activate account",
|
||||
"deactivate_account": "Deactivate account",
|
||||
"delete_account": "Delete account",
|
||||
"force_nsfw": "Mark all posts as NSFW",
|
||||
"strip_media": "Remove media from posts",
|
||||
"force_unlisted": "Force posts to be unlisted",
|
||||
"sandbox": "Force posts to be followers-only",
|
||||
"disable_remote_subscription": "Disallow following user from remote instances",
|
||||
"disable_any_subscription": "Disallow following user at all",
|
||||
"quarantine": "Disallow user posts from federating",
|
||||
"delete_user": "Delete user",
|
||||
"delete_user_confirmation": "Are you absolutely sure? This action cannot be undone."
|
||||
}
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "User Timeline",
|
||||
|
|
|
@ -20,7 +20,10 @@
|
|||
"submit": "Mandar",
|
||||
"more": "Mai",
|
||||
"generic_error": "Una error s’es producha",
|
||||
"optional": "opcional"
|
||||
"optional": "opcional",
|
||||
"show_more": "Mostrar mai",
|
||||
"show_less": "Mostrar mens",
|
||||
"cancel": "Anullar"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Talhar l’imatge",
|
||||
|
@ -74,11 +77,13 @@
|
|||
"content_type": {
|
||||
"text/plain": "Tèxte brut",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown"
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "Avís de contengut (opcional)",
|
||||
"default": "Escrivètz aquí vòstre estatut.",
|
||||
"direct_warning": "Aquesta publicacion serà pas que visibla pels utilizaires mencionats.",
|
||||
"direct_warning_to_all": "Aquesta publicacion serà pas que visibla pels utilizaires mencionats.",
|
||||
"direct_warning_to_first_only": "Aquesta publicacion serà pas que visibla pels utilizaires mencionats a la debuta del messatge.",
|
||||
"posting": "Mandadís",
|
||||
"scope": {
|
||||
"direct": "Dirècte - Publicar pels utilizaires mencionats solament",
|
||||
|
@ -108,6 +113,9 @@
|
|||
"password_confirmation_match": "deu èsser lo meteis senhal"
|
||||
}
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "O seleccionar tot"
|
||||
},
|
||||
"settings": {
|
||||
"app_name": "Nom de l’aplicacion",
|
||||
"attachmentRadius": "Pèças juntas",
|
||||
|
@ -213,8 +221,11 @@
|
|||
"reply_visibility_self": "Mostrar pas que las responsas que me son destinadas",
|
||||
"saving_err": "Error en enregistrant los paramètres",
|
||||
"saving_ok": "Paramètres enregistrats",
|
||||
"scope_copy": "Copiar lo nivèl de confidencialitat per las responsas (Totjorn aissí pels Messatges Dirèctes)",
|
||||
"search_user_to_block": "Cercatz qual volètz blocar",
|
||||
"search_user_to_mute": "Cercatz qual volètz rescondre",
|
||||
"security_tab": "Seguretat",
|
||||
"scope_copy": "Copiar lo nivèl de confidencialitat per las responsas (Totjorn aissí pels Messatges Dirèctes)",
|
||||
"minimal_scopes_mode": "Minimizar lo nombre d’opcions per publicacion",
|
||||
"set_new_avatar": "Definir un nòu avatar",
|
||||
"set_new_profile_background": "Definir un nòu fons de perfil",
|
||||
"set_new_profile_banner": "Definir una nòva bandièra de perfil",
|
||||
|
@ -349,6 +360,11 @@
|
|||
"checkbox": "Ai legit los tèrmes e condicions d’utilizacion",
|
||||
"link": "un pichon ligam simpatic"
|
||||
}
|
||||
},
|
||||
"version": {
|
||||
"title": "Version",
|
||||
"backend_version": "Version Backend",
|
||||
"frontend_version": "Version Frontend"
|
||||
}
|
||||
},
|
||||
"timeline": {
|
||||
|
@ -394,7 +410,26 @@
|
|||
"block_progress": "Blocatge...",
|
||||
"unmute": "Tornar mostrar",
|
||||
"unmute_progress": "Afichatge...",
|
||||
"mute_progress": "A amagar..."
|
||||
"mute_progress": "A amagar...",
|
||||
"admin_menu": {
|
||||
"moderation": "Moderacion",
|
||||
"grant_admin": "Passar Admin",
|
||||
"revoke_admin": "Revocar Admin",
|
||||
"grant_moderator": "Passar Moderator",
|
||||
"revoke_moderator": "Revocar Moderator",
|
||||
"activate_account": "Activar lo compte",
|
||||
"deactivate_account": "Desactivar lo compte",
|
||||
"delete_account": "Suprimir lo compte",
|
||||
"force_nsfw": "Marcar totas las publicacions coma sensiblas",
|
||||
"strip_media": "Tirar los mèdias de las publicacions",
|
||||
"force_unlisted": "Forçar las publicacions en pas-listadas",
|
||||
"sandbox": "Forçar las publicacions en seguidors solament",
|
||||
"disable_remote_subscription": "Desactivar lo seguiment d’utilizaire d’instàncias alonhadas",
|
||||
"disable_any_subscription": "Desactivar tot seguiment",
|
||||
"quarantine": "Defendre la federacion de las publicacions de l’utilizaire",
|
||||
"delete_user": "Suprimir l’utilizaire",
|
||||
"delete_user_confirmation": "Volètz vertadièrament far aquò ? Aquesta accion se pòt pas anullar."
|
||||
}
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "Flux utilizaire",
|
||||
|
|
|
@ -74,7 +74,8 @@
|
|||
"content_type": {
|
||||
"text/plain": "Czysty tekst",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown"
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "Temat (nieobowiązkowy)",
|
||||
"default": "Właśnie wróciłem z kościoła",
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
},
|
||||
"general": {
|
||||
"apply": "Применить",
|
||||
"submit": "Отправить"
|
||||
"submit": "Отправить",
|
||||
"cancel": "Отмена"
|
||||
},
|
||||
"login": {
|
||||
"login": "Войти",
|
||||
|
@ -311,7 +312,26 @@
|
|||
"muted": "Игнорирую",
|
||||
"per_day": "в день",
|
||||
"remote_follow": "Читать удалённо",
|
||||
"statuses": "Статусы"
|
||||
"statuses": "Статусы",
|
||||
"admin_menu": {
|
||||
"moderation": "Опции модератора",
|
||||
"grant_admin": "Сделать администратором",
|
||||
"revoke_admin": "Забрать права администратора",
|
||||
"grant_moderator": "Сделать модератором",
|
||||
"revoke_moderator": "Забрать права модератора",
|
||||
"activate_account": "Активировать аккаунт",
|
||||
"deactivate_account": "Деактивировать аккаунт",
|
||||
"delete_account": "Удалить аккаунт",
|
||||
"force_nsfw": "Отмечать посты пользователя как NSFW",
|
||||
"strip_media": "Убирать вложения из постов пользователя",
|
||||
"force_unlisted": "Не добавлять посты в публичные ленты",
|
||||
"sandbox": "Посты доступны только для подписчиков",
|
||||
"disable_remote_subscription": "Запретить подписываться с удаленных серверов",
|
||||
"disable_any_subscription": "Запретить подписываться на пользователя",
|
||||
"quarantine": "Не федерировать посты пользователя",
|
||||
"delete_user": "Удалить пользователя",
|
||||
"delete_user_confirmation": "Вы уверены? Это действие нельзя отменить."
|
||||
}
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "Лента пользователя"
|
||||
|
|
|
@ -22,6 +22,7 @@ import pushNotifications from './lib/push_notifications_plugin.js'
|
|||
import messages from './i18n/messages.js'
|
||||
|
||||
import VueChatScroll from 'vue-chat-scroll'
|
||||
import VueClickOutside from 'v-click-outside'
|
||||
|
||||
import afterStoreSetup from './boot/after_store.js'
|
||||
|
||||
|
@ -39,6 +40,7 @@ Vue.use(VueTimeago, {
|
|||
})
|
||||
Vue.use(VueI18n)
|
||||
Vue.use(VueChatScroll)
|
||||
Vue.use(VueClickOutside)
|
||||
|
||||
const i18n = new VueI18n({
|
||||
// By default, use the browser locale, we will update it if neccessary
|
||||
|
@ -59,6 +61,11 @@ const persistedStateOptions = {
|
|||
const persistedState = await createPersistedState(persistedStateOptions)
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
i18n: {
|
||||
getters: {
|
||||
i18n: () => i18n
|
||||
}
|
||||
},
|
||||
interface: interfaceModule,
|
||||
instance: instanceModule,
|
||||
statuses: statusesModule,
|
||||
|
|
|
@ -13,11 +13,11 @@ const api = {
|
|||
setBackendInteractor (state, backendInteractor) {
|
||||
state.backendInteractor = backendInteractor
|
||||
},
|
||||
addFetcher (state, {timeline, fetcher}) {
|
||||
state.fetchers[timeline] = fetcher
|
||||
addFetcher (state, { fetcherName, fetcher }) {
|
||||
state.fetchers[fetcherName] = fetcher
|
||||
},
|
||||
removeFetcher (state, {timeline}) {
|
||||
delete state.fetchers[timeline]
|
||||
removeFetcher (state, { fetcherName }) {
|
||||
delete state.fetchers[fetcherName]
|
||||
},
|
||||
setWsToken (state, token) {
|
||||
state.wsToken = token
|
||||
|
@ -33,17 +33,24 @@ const api = {
|
|||
}
|
||||
},
|
||||
actions: {
|
||||
startFetching (store, {timeline = 'friends', tag = false, userId = false}) {
|
||||
startFetchingTimeline (store, { timeline = 'friends', tag = false, userId = false }) {
|
||||
// Don't start fetching if we already are.
|
||||
if (store.state.fetchers[timeline]) return
|
||||
|
||||
const fetcher = store.state.backendInteractor.startFetching({ timeline, store, userId, tag })
|
||||
store.commit('addFetcher', { timeline, fetcher })
|
||||
const fetcher = store.state.backendInteractor.startFetchingTimeline({ timeline, store, userId, tag })
|
||||
store.commit('addFetcher', { fetcherName: timeline, fetcher })
|
||||
},
|
||||
stopFetching (store, timeline) {
|
||||
const fetcher = store.state.fetchers[timeline]
|
||||
startFetchingNotifications (store) {
|
||||
// Don't start fetching if we already are.
|
||||
if (store.state.fetchers['notifications']) return
|
||||
|
||||
const fetcher = store.state.backendInteractor.startFetchingNotifications({ store })
|
||||
store.commit('addFetcher', { fetcherName: 'notifications', fetcher })
|
||||
},
|
||||
stopFetching (store, fetcherName) {
|
||||
const fetcher = store.state.fetchers[fetcherName]
|
||||
window.clearInterval(fetcher)
|
||||
store.commit('removeFetcher', {timeline})
|
||||
store.commit('removeFetcher', { fetcherName })
|
||||
},
|
||||
setWsToken (store, token) {
|
||||
store.commit('setWsToken', token)
|
||||
|
|
|
@ -48,7 +48,6 @@ const interfaceMod = {
|
|||
commit('setNotificationPermission', permission)
|
||||
},
|
||||
setMobileLayout ({ commit }, value) {
|
||||
console.log('setMobileLayout called')
|
||||
commit('setMobileLayout', value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,20 +20,22 @@ const emptyTl = (userId = 0) => ({
|
|||
flushMarker: 0
|
||||
})
|
||||
|
||||
export const defaultState = () => ({
|
||||
allStatuses: [],
|
||||
allStatusesObject: {},
|
||||
maxId: 0,
|
||||
notifications: {
|
||||
const emptyNotifications = () => ({
|
||||
desktopNotificationSilence: true,
|
||||
maxId: 0,
|
||||
minId: Number.POSITIVE_INFINITY,
|
||||
data: [],
|
||||
idStore: {},
|
||||
loading: false,
|
||||
error: false,
|
||||
fetcherId: null
|
||||
},
|
||||
error: false
|
||||
})
|
||||
|
||||
export const defaultState = () => ({
|
||||
allStatuses: [],
|
||||
allStatusesObject: {},
|
||||
conversationsObject: {},
|
||||
maxId: 0,
|
||||
notifications: emptyNotifications(),
|
||||
favorites: new Set(),
|
||||
error: false,
|
||||
timelines: {
|
||||
|
@ -111,6 +113,39 @@ const sortTimeline = (timeline) => {
|
|||
return timeline
|
||||
}
|
||||
|
||||
// Add status to the global storages (arrays and objects maintaining statuses) except timelines
|
||||
const addStatusToGlobalStorage = (state, data) => {
|
||||
const result = mergeOrAdd(state.allStatuses, state.allStatusesObject, data)
|
||||
if (result.new) {
|
||||
// Add to conversation
|
||||
const status = result.item
|
||||
const conversationsObject = state.conversationsObject
|
||||
const conversationId = status.statusnet_conversation_id
|
||||
if (conversationsObject[conversationId]) {
|
||||
conversationsObject[conversationId].push(status)
|
||||
} else {
|
||||
set(conversationsObject, conversationId, [status])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Remove status from the global storages (arrays and objects maintaining statuses) except timelines
|
||||
const removeStatusFromGlobalStorage = (state, status) => {
|
||||
remove(state.allStatuses, { id: status.id })
|
||||
|
||||
// TODO: Need to remove from allStatusesObject?
|
||||
|
||||
// Remove possible notification
|
||||
remove(state.notifications.data, ({action: {id}}) => id === status.id)
|
||||
|
||||
// Remove from conversation
|
||||
const conversationId = status.statusnet_conversation_id
|
||||
if (state.conversationsObject[conversationId]) {
|
||||
remove(state.conversationsObject[conversationId], { id: status.id })
|
||||
}
|
||||
}
|
||||
|
||||
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId }) => {
|
||||
// Sanity check
|
||||
if (!isArray(statuses)) {
|
||||
|
@ -118,7 +153,6 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
}
|
||||
|
||||
const allStatuses = state.allStatuses
|
||||
const allStatusesObject = state.allStatusesObject
|
||||
const timelineObject = state.timelines[timeline]
|
||||
|
||||
const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0
|
||||
|
@ -141,7 +175,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
}
|
||||
|
||||
const addStatus = (data, showImmediately, addToTimeline = true) => {
|
||||
const result = mergeOrAdd(allStatuses, allStatusesObject, data)
|
||||
const result = addStatusToGlobalStorage(state, data)
|
||||
const status = result.item
|
||||
|
||||
if (result.new) {
|
||||
|
@ -235,16 +269,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
},
|
||||
'deletion': (deletion) => {
|
||||
const uri = deletion.uri
|
||||
|
||||
// Remove possible notification
|
||||
const status = find(allStatuses, {uri})
|
||||
if (!status) {
|
||||
return
|
||||
}
|
||||
|
||||
remove(state.notifications.data, ({action: {id}}) => id === status.id)
|
||||
removeStatusFromGlobalStorage(state, status)
|
||||
|
||||
remove(allStatuses, { uri })
|
||||
if (timeline) {
|
||||
remove(timelineObject.statuses, { uri })
|
||||
remove(timelineObject.visibleStatuses, { uri })
|
||||
|
@ -271,12 +302,12 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
}
|
||||
}
|
||||
|
||||
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes }) => {
|
||||
const allStatuses = state.allStatuses
|
||||
const allStatusesObject = state.allStatusesObject
|
||||
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
|
||||
each(notifications, (notification) => {
|
||||
notification.action = mergeOrAdd(allStatuses, allStatusesObject, notification.action).item
|
||||
notification.status = notification.status && mergeOrAdd(allStatuses, allStatusesObject, notification.status).item
|
||||
if (notification.type !== 'follow') {
|
||||
notification.action = addStatusToGlobalStorage(state, notification.action).item
|
||||
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
|
||||
}
|
||||
|
||||
// Only add a new notification if we don't have one for the same action
|
||||
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
|
||||
|
@ -292,15 +323,32 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
|||
|
||||
if ('Notification' in window && window.Notification.permission === 'granted') {
|
||||
const notifObj = {}
|
||||
const action = notification.action
|
||||
const title = action.user.name
|
||||
notifObj.icon = action.user.profile_image_url
|
||||
notifObj.body = action.text // there's a problem that it doesn't put a space before links tho
|
||||
const status = notification.status
|
||||
const title = notification.from_profile.name
|
||||
notifObj.icon = notification.from_profile.profile_image_url
|
||||
let i18nString
|
||||
switch (notification.type) {
|
||||
case 'like':
|
||||
i18nString = 'favorited_you'
|
||||
break
|
||||
case 'repeat':
|
||||
i18nString = 'repeated_you'
|
||||
break
|
||||
case 'follow':
|
||||
i18nString = 'followed_you'
|
||||
break
|
||||
}
|
||||
|
||||
if (i18nString) {
|
||||
notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
|
||||
} else {
|
||||
notifObj.body = notification.status.text
|
||||
}
|
||||
|
||||
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
|
||||
if (action.attachments && action.attachments.length > 0 && !action.nsfw &&
|
||||
action.attachments[0].mimetype.startsWith('image/')) {
|
||||
notifObj.image = action.attachments[0].url
|
||||
if (status && status.attachments && status.attachments.length > 0 && !status.nsfw &&
|
||||
status.attachments[0].mimetype.startsWith('image/')) {
|
||||
notifObj.image = status.attachments[0].url
|
||||
}
|
||||
|
||||
if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) {
|
||||
|
@ -340,9 +388,6 @@ export const mutations = {
|
|||
oldTimeline.visibleStatusesObject = {}
|
||||
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
|
||||
},
|
||||
setNotificationFetcher (state, { fetcherId }) {
|
||||
state.notifications.fetcherId = fetcherId
|
||||
},
|
||||
resetStatuses (state) {
|
||||
const emptyState = defaultState()
|
||||
Object.entries(emptyState).forEach(([key, value]) => {
|
||||
|
@ -352,6 +397,9 @@ export const mutations = {
|
|||
clearTimeline (state, { timeline }) {
|
||||
state.timelines[timeline] = emptyTl(state.timelines[timeline].userId)
|
||||
},
|
||||
clearNotifications (state) {
|
||||
state.notifications = emptyNotifications()
|
||||
},
|
||||
setFavorited (state, { status, value }) {
|
||||
const newStatus = state.allStatusesObject[status.id]
|
||||
newStatus.favorited = value
|
||||
|
@ -378,6 +426,13 @@ export const mutations = {
|
|||
const newStatus = state.allStatusesObject[status.id]
|
||||
newStatus.deleted = true
|
||||
},
|
||||
setManyDeleted (state, condition) {
|
||||
Object.values(state.allStatusesObject).forEach(status => {
|
||||
if (condition(status)) {
|
||||
status.deleted = true
|
||||
}
|
||||
})
|
||||
},
|
||||
setLoading (state, { timeline, value }) {
|
||||
state.timelines[timeline].loading = value
|
||||
},
|
||||
|
@ -413,8 +468,8 @@ const statuses = {
|
|||
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) {
|
||||
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId })
|
||||
},
|
||||
addNewNotifications ({ rootState, commit, dispatch }, { notifications, older }) {
|
||||
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older })
|
||||
addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) {
|
||||
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
|
||||
},
|
||||
setError ({ rootState, commit }, { value }) {
|
||||
commit('setError', { value })
|
||||
|
@ -428,16 +483,13 @@ const statuses = {
|
|||
setNotificationsSilence ({ rootState, commit }, { value }) {
|
||||
commit('setNotificationsSilence', { value })
|
||||
},
|
||||
stopFetchingNotifications ({ rootState, commit }) {
|
||||
if (rootState.statuses.notifications.fetcherId) {
|
||||
window.clearInterval(rootState.statuses.notifications.fetcherId)
|
||||
}
|
||||
commit('setNotificationFetcher', { fetcherId: null })
|
||||
},
|
||||
deleteStatus ({ rootState, commit }, status) {
|
||||
commit('setDeleted', { status })
|
||||
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
|
||||
},
|
||||
markStatusesAsDeleted ({ commit }, condition) {
|
||||
commit('setManyDeleted', condition)
|
||||
},
|
||||
favorite ({ rootState, commit }, status) {
|
||||
// Optimistic favoriting...
|
||||
commit('setFavorited', { status, value: true })
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { compact, map, each, merge, find, last } from 'lodash'
|
||||
import userSearchApi from '../services/new_api/user_search.js'
|
||||
import { compact, map, each, merge, last, concat, uniq } from 'lodash'
|
||||
import { set } from 'vue'
|
||||
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
|
||||
import oauthApi from '../services/new_api/oauth'
|
||||
|
@ -32,11 +33,62 @@ const getNotificationPermission = () => {
|
|||
return Promise.resolve(Notification.permission)
|
||||
}
|
||||
|
||||
const blockUser = (store, id) => {
|
||||
return store.rootState.api.backendInteractor.blockUser(id)
|
||||
.then((relationship) => {
|
||||
store.commit('updateUserRelationship', [relationship])
|
||||
store.commit('addBlockId', id)
|
||||
store.commit('removeStatus', { timeline: 'friends', userId: id })
|
||||
store.commit('removeStatus', { timeline: 'public', userId: id })
|
||||
store.commit('removeStatus', { timeline: 'publicAndExternal', userId: id })
|
||||
})
|
||||
}
|
||||
|
||||
const unblockUser = (store, id) => {
|
||||
return store.rootState.api.backendInteractor.unblockUser(id)
|
||||
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
||||
}
|
||||
|
||||
const muteUser = (store, id) => {
|
||||
return store.rootState.api.backendInteractor.muteUser(id)
|
||||
.then((relationship) => {
|
||||
store.commit('updateUserRelationship', [relationship])
|
||||
store.commit('addMuteId', id)
|
||||
})
|
||||
}
|
||||
|
||||
const unmuteUser = (store, id) => {
|
||||
return store.rootState.api.backendInteractor.unmuteUser(id)
|
||||
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
setMuted (state, { user: { id }, muted }) {
|
||||
const user = state.usersObject[id]
|
||||
set(user, 'muted', muted)
|
||||
},
|
||||
tagUser (state, { user: { id }, tag }) {
|
||||
const user = state.usersObject[id]
|
||||
const tags = user.tags || []
|
||||
const newTags = tags.concat([tag])
|
||||
set(user, 'tags', newTags)
|
||||
},
|
||||
untagUser (state, { user: { id }, tag }) {
|
||||
const user = state.usersObject[id]
|
||||
const tags = user.tags || []
|
||||
const newTags = tags.filter(t => t !== tag)
|
||||
set(user, 'tags', newTags)
|
||||
},
|
||||
updateRight (state, { user: { id }, right, value }) {
|
||||
const user = state.usersObject[id]
|
||||
let newRights = user.rights
|
||||
newRights[right] = value
|
||||
set(user, 'rights', newRights)
|
||||
},
|
||||
updateActivationStatus (state, { user: { id }, status }) {
|
||||
const user = state.usersObject[id]
|
||||
set(user, 'deactivated', !status)
|
||||
},
|
||||
setCurrentUser (state, user) {
|
||||
state.lastLoginName = user.screen_name
|
||||
state.currentUser = merge(state.currentUser || {}, user)
|
||||
|
@ -51,42 +103,27 @@ export const mutations = {
|
|||
endLogin (state) {
|
||||
state.loggingIn = false
|
||||
},
|
||||
// TODO Clean after ourselves?
|
||||
addFriends (state, { id, friends }) {
|
||||
saveFriendIds (state, { id, friendIds }) {
|
||||
const user = state.usersObject[id]
|
||||
each(friends, friend => {
|
||||
if (!find(user.friends, { id: friend.id })) {
|
||||
user.friends.push(friend)
|
||||
}
|
||||
})
|
||||
user.lastFriendId = last(friends).id
|
||||
user.friendIds = uniq(concat(user.friendIds, friendIds))
|
||||
},
|
||||
addFollowers (state, { id, followers }) {
|
||||
saveFollowerIds (state, { id, followerIds }) {
|
||||
const user = state.usersObject[id]
|
||||
each(followers, follower => {
|
||||
if (!find(user.followers, { id: follower.id })) {
|
||||
user.followers.push(follower)
|
||||
}
|
||||
})
|
||||
user.lastFollowerId = last(followers).id
|
||||
user.followerIds = uniq(concat(user.followerIds, followerIds))
|
||||
},
|
||||
// Because frontend doesn't have a reason to keep these stuff in memory
|
||||
// outside of viewing someones user profile.
|
||||
clearFriends (state, userId) {
|
||||
const user = state.usersObject[userId]
|
||||
if (!user) {
|
||||
return
|
||||
if (user) {
|
||||
set(user, 'friendIds', [])
|
||||
}
|
||||
user.friends = []
|
||||
user.lastFriendId = null
|
||||
},
|
||||
clearFollowers (state, userId) {
|
||||
const user = state.usersObject[userId]
|
||||
if (!user) {
|
||||
return
|
||||
if (user) {
|
||||
set(user, 'followerIds', [])
|
||||
}
|
||||
user.followers = []
|
||||
user.lastFollowerId = null
|
||||
},
|
||||
addNewUsers (state, users) {
|
||||
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
|
||||
|
@ -110,6 +147,11 @@ export const mutations = {
|
|||
saveBlockIds (state, blockIds) {
|
||||
state.currentUser.blockIds = blockIds
|
||||
},
|
||||
addBlockId (state, blockId) {
|
||||
if (state.currentUser.blockIds.indexOf(blockId) === -1) {
|
||||
state.currentUser.blockIds.push(blockId)
|
||||
}
|
||||
},
|
||||
updateMutes (state, mutedUsers) {
|
||||
// Reset muted of all fetched users
|
||||
each(state.users, (user) => { user.muted = false })
|
||||
|
@ -118,12 +160,19 @@ export const mutations = {
|
|||
saveMuteIds (state, muteIds) {
|
||||
state.currentUser.muteIds = muteIds
|
||||
},
|
||||
addMuteId (state, muteId) {
|
||||
if (state.currentUser.muteIds.indexOf(muteId) === -1) {
|
||||
state.currentUser.muteIds.push(muteId)
|
||||
}
|
||||
},
|
||||
setUserForStatus (state, status) {
|
||||
status.user = state.usersObject[status.user.id]
|
||||
},
|
||||
setUserForNotification (state, notification) {
|
||||
if (notification.type !== 'follow') {
|
||||
notification.action.user = state.usersObject[notification.action.user.id]
|
||||
notification.from_profile = state.usersObject[notification.action.user.id]
|
||||
}
|
||||
notification.from_profile = state.usersObject[notification.from_profile.id]
|
||||
},
|
||||
setColor (state, { user: { id }, highlighted }) {
|
||||
const user = state.usersObject[id]
|
||||
|
@ -176,8 +225,10 @@ const users = {
|
|||
})
|
||||
},
|
||||
fetchUserRelationship (store, id) {
|
||||
return store.rootState.api.backendInteractor.fetchUserRelationship({ id })
|
||||
if (store.state.currentUser) {
|
||||
store.rootState.api.backendInteractor.fetchUserRelationship({ id })
|
||||
.then((relationships) => store.commit('updateUserRelationship', relationships))
|
||||
}
|
||||
},
|
||||
fetchBlocks (store) {
|
||||
return store.rootState.api.backendInteractor.fetchBlocks()
|
||||
|
@ -187,18 +238,17 @@ const users = {
|
|||
return blocks
|
||||
})
|
||||
},
|
||||
blockUser (store, userId) {
|
||||
return store.rootState.api.backendInteractor.blockUser(userId)
|
||||
.then((relationship) => {
|
||||
store.commit('updateUserRelationship', [relationship])
|
||||
store.commit('removeStatus', { timeline: 'friends', userId })
|
||||
store.commit('removeStatus', { timeline: 'public', userId })
|
||||
store.commit('removeStatus', { timeline: 'publicAndExternal', userId })
|
||||
})
|
||||
blockUser (store, id) {
|
||||
return blockUser(store, id)
|
||||
},
|
||||
unblockUser (store, id) {
|
||||
return store.rootState.api.backendInteractor.unblockUser(id)
|
||||
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
||||
return unblockUser(store, id)
|
||||
},
|
||||
blockUsers (store, ids = []) {
|
||||
return Promise.all(ids.map(id => blockUser(store, id)))
|
||||
},
|
||||
unblockUsers (store, ids = []) {
|
||||
return Promise.all(ids.map(id => unblockUser(store, id)))
|
||||
},
|
||||
fetchMutes (store) {
|
||||
return store.rootState.api.backendInteractor.fetchMutes()
|
||||
|
@ -209,32 +259,34 @@ const users = {
|
|||
})
|
||||
},
|
||||
muteUser (store, id) {
|
||||
return store.rootState.api.backendInteractor.muteUser(id)
|
||||
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
||||
return muteUser(store, id)
|
||||
},
|
||||
unmuteUser (store, id) {
|
||||
return store.rootState.api.backendInteractor.unmuteUser(id)
|
||||
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
||||
return unmuteUser(store, id)
|
||||
},
|
||||
addFriends ({ rootState, commit }, fetchBy) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const user = rootState.users.usersObject[fetchBy]
|
||||
const maxId = user.lastFriendId
|
||||
rootState.api.backendInteractor.fetchFriends({ id: user.id, maxId })
|
||||
muteUsers (store, ids = []) {
|
||||
return Promise.all(ids.map(id => muteUser(store, id)))
|
||||
},
|
||||
unmuteUsers (store, ids = []) {
|
||||
return Promise.all(ids.map(id => unmuteUser(store, id)))
|
||||
},
|
||||
fetchFriends ({ rootState, commit }, id) {
|
||||
const user = rootState.users.usersObject[id]
|
||||
const maxId = last(user.friendIds)
|
||||
return rootState.api.backendInteractor.fetchFriends({ id, maxId })
|
||||
.then((friends) => {
|
||||
commit('addFriends', { id: user.id, friends })
|
||||
resolve(friends)
|
||||
}).catch(() => {
|
||||
reject()
|
||||
})
|
||||
commit('addNewUsers', friends)
|
||||
commit('saveFriendIds', { id, friendIds: map(friends, 'id') })
|
||||
return friends
|
||||
})
|
||||
},
|
||||
addFollowers ({ rootState, commit }, fetchBy) {
|
||||
const user = rootState.users.usersObject[fetchBy]
|
||||
const maxId = user.lastFollowerId
|
||||
return rootState.api.backendInteractor.fetchFollowers({ id: user.id, maxId })
|
||||
fetchFollowers ({ rootState, commit }, id) {
|
||||
const user = rootState.users.usersObject[id]
|
||||
const maxId = last(user.followerIds)
|
||||
return rootState.api.backendInteractor.fetchFollowers({ id, maxId })
|
||||
.then((followers) => {
|
||||
commit('addFollowers', { id: user.id, followers })
|
||||
commit('addNewUsers', followers)
|
||||
commit('saveFollowerIds', { id, followerIds: map(followers, 'id') })
|
||||
return followers
|
||||
})
|
||||
},
|
||||
|
@ -257,6 +309,9 @@ const users = {
|
|||
|
||||
unregisterPushNotifications(token)
|
||||
},
|
||||
addNewUsers ({ commit }, users) {
|
||||
commit('addNewUsers', users)
|
||||
},
|
||||
addNewStatuses (store, { statuses }) {
|
||||
const users = map(statuses, 'user')
|
||||
const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
|
||||
|
@ -287,6 +342,14 @@ const users = {
|
|||
store.commit('setUserForNotification', notification)
|
||||
})
|
||||
},
|
||||
searchUsers (store, query) {
|
||||
// TODO: Move userSearch api into api.service
|
||||
return userSearchApi.search({query, store: { state: store.rootState }})
|
||||
.then((users) => {
|
||||
store.commit('addNewUsers', users)
|
||||
return users
|
||||
})
|
||||
},
|
||||
async signUp (store, userInfo) {
|
||||
store.commit('signUpPending')
|
||||
|
||||
|
@ -331,7 +394,8 @@ const users = {
|
|||
store.commit('setToken', false)
|
||||
store.dispatch('stopFetching', 'friends')
|
||||
store.commit('setBackendInteractor', backendInteractorService())
|
||||
store.dispatch('stopFetchingNotifications')
|
||||
store.dispatch('stopFetching', 'notifications')
|
||||
store.commit('clearNotifications')
|
||||
store.commit('resetStatuses')
|
||||
},
|
||||
loginUser (store, accessToken) {
|
||||
|
@ -363,7 +427,10 @@ const users = {
|
|||
}
|
||||
|
||||
// Start getting fresh posts.
|
||||
store.dispatch('startFetching', { timeline: 'friends' })
|
||||
store.dispatch('startFetchingTimeline', { timeline: 'friends' })
|
||||
|
||||
// Start fetching notifications
|
||||
store.dispatch('startFetchingNotifications')
|
||||
|
||||
// Get user mutes
|
||||
store.dispatch('fetchMutes')
|
||||
|
|
|
@ -8,7 +8,6 @@ const BG_UPDATE_URL = '/api/qvitter/update_background_image.json'
|
|||
const BANNER_UPDATE_URL = '/api/account/update_profile_banner.json'
|
||||
const PROFILE_UPDATE_URL = '/api/account/update_profile.json'
|
||||
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
|
||||
const QVITTER_USER_NOTIFICATIONS_URL = '/api/qvitter/statuses/notifications.json'
|
||||
const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
|
||||
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
|
||||
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
|
||||
|
@ -16,9 +15,14 @@ const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
|
|||
const FOLLOW_REQUESTS_URL = '/api/pleroma/friend_requests'
|
||||
const APPROVE_USER_URL = '/api/pleroma/friendships/approve'
|
||||
const DENY_USER_URL = '/api/pleroma/friendships/deny'
|
||||
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
|
||||
const PERMISSION_GROUP_URL = '/api/pleroma/admin/permission_group'
|
||||
const ACTIVATION_STATUS_URL = '/api/pleroma/admin/activation_status'
|
||||
const ADMIN_USER_URL = '/api/pleroma/admin/user'
|
||||
const SUGGESTIONS_URL = '/api/v1/suggestions'
|
||||
|
||||
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
|
||||
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
|
||||
const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
|
||||
const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
|
||||
const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog`
|
||||
|
@ -46,7 +50,7 @@ const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
|
|||
const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
|
||||
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
|
||||
|
||||
import { each, map } from 'lodash'
|
||||
import { each, map, concat, last } from 'lodash'
|
||||
import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
import 'whatwg-fetch'
|
||||
import { StatusCodeError } from '../errors/errors'
|
||||
|
@ -290,10 +294,23 @@ const fetchFriends = ({id, maxId, sinceId, limit = 20, credentials}) => {
|
|||
}
|
||||
|
||||
const exportFriends = ({id, credentials}) => {
|
||||
let url = MASTODON_FOLLOWING_URL(id) + `?all=true`
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((data) => data.json())
|
||||
.then((data) => data.map(parseUser))
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
let friends = []
|
||||
let more = true
|
||||
while (more) {
|
||||
const maxId = friends.length > 0 ? last(friends).id : undefined
|
||||
const users = await fetchFriends({id, maxId, credentials})
|
||||
friends = concat(friends, users)
|
||||
if (users.length === 0) {
|
||||
more = false
|
||||
}
|
||||
}
|
||||
resolve(friends)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const fetchFollowers = ({id, maxId, sinceId, limit = 20, credentials}) => {
|
||||
|
@ -352,13 +369,93 @@ const fetchStatus = ({id, credentials}) => {
|
|||
.then((data) => parseStatus(data))
|
||||
}
|
||||
|
||||
const tagUser = ({tag, credentials, ...options}) => {
|
||||
const screenName = options.screen_name
|
||||
const form = {
|
||||
nicknames: [screenName],
|
||||
tags: [tag]
|
||||
}
|
||||
|
||||
const headers = authHeaders(credentials)
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
return fetch(TAG_USER_URL, {
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
body: JSON.stringify(form)
|
||||
})
|
||||
}
|
||||
|
||||
const untagUser = ({tag, credentials, ...options}) => {
|
||||
const screenName = options.screen_name
|
||||
const body = {
|
||||
nicknames: [screenName],
|
||||
tags: [tag]
|
||||
}
|
||||
|
||||
const headers = authHeaders(credentials)
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
return fetch(TAG_USER_URL, {
|
||||
method: 'DELETE',
|
||||
headers: headers,
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
}
|
||||
|
||||
const addRight = ({right, credentials, ...user}) => {
|
||||
const screenName = user.screen_name
|
||||
|
||||
return fetch(`${PERMISSION_GROUP_URL}/${screenName}/${right}`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(credentials),
|
||||
body: {}
|
||||
})
|
||||
}
|
||||
|
||||
const deleteRight = ({right, credentials, ...user}) => {
|
||||
const screenName = user.screen_name
|
||||
|
||||
return fetch(`${PERMISSION_GROUP_URL}/${screenName}/${right}`, {
|
||||
method: 'DELETE',
|
||||
headers: authHeaders(credentials),
|
||||
body: {}
|
||||
})
|
||||
}
|
||||
|
||||
const setActivationStatus = ({status, credentials, ...user}) => {
|
||||
const screenName = user.screen_name
|
||||
const body = {
|
||||
status: status
|
||||
}
|
||||
|
||||
const headers = authHeaders(credentials)
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
return fetch(`${ACTIVATION_STATUS_URL}/${screenName}.json`, {
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
body: JSON.stringify(body)
|
||||
})
|
||||
}
|
||||
|
||||
const deleteUser = ({credentials, ...user}) => {
|
||||
const screenName = user.screen_name
|
||||
const headers = authHeaders(credentials)
|
||||
|
||||
return fetch(`${ADMIN_USER_URL}.json?nickname=${screenName}`, {
|
||||
method: 'DELETE',
|
||||
headers: headers
|
||||
})
|
||||
}
|
||||
|
||||
const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => {
|
||||
const timelineUrls = {
|
||||
public: MASTODON_PUBLIC_TIMELINE,
|
||||
friends: MASTODON_USER_HOME_TIMELINE_URL,
|
||||
mentions: MENTIONS_URL,
|
||||
dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
|
||||
notifications: QVITTER_USER_NOTIFICATIONS_URL,
|
||||
notifications: MASTODON_USER_NOTIFICATIONS_URL,
|
||||
'publicAndExternal': MASTODON_PUBLIC_TIMELINE,
|
||||
user: MASTODON_USER_TIMELINE_URL,
|
||||
media: MASTODON_USER_TIMELINE_URL,
|
||||
|
@ -666,6 +763,12 @@ const apiService = {
|
|||
fetchBlocks,
|
||||
fetchOAuthTokens,
|
||||
revokeOAuthToken,
|
||||
tagUser,
|
||||
untagUser,
|
||||
deleteUser,
|
||||
addRight,
|
||||
deleteRight,
|
||||
setActivationStatus,
|
||||
register,
|
||||
getCaptcha,
|
||||
updateAvatar,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import apiService from '../api/api.service.js'
|
||||
import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js'
|
||||
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
|
||||
|
||||
const backendInteractorService = (credentials) => {
|
||||
const fetchStatus = ({id}) => {
|
||||
|
@ -58,8 +59,36 @@ const backendInteractorService = (credentials) => {
|
|||
return apiService.denyUser({credentials, id})
|
||||
}
|
||||
|
||||
const startFetching = ({timeline, store, userId = false, tag}) => {
|
||||
return timelineFetcherService.startFetching({timeline, store, credentials, userId, tag})
|
||||
const startFetchingTimeline = ({ timeline, store, userId = false, tag }) => {
|
||||
return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag })
|
||||
}
|
||||
|
||||
const startFetchingNotifications = ({ store }) => {
|
||||
return notificationsFetcher.startFetching({ store, credentials })
|
||||
}
|
||||
|
||||
const tagUser = ({screen_name}, tag) => {
|
||||
return apiService.tagUser({screen_name, tag, credentials})
|
||||
}
|
||||
|
||||
const untagUser = ({screen_name}, tag) => {
|
||||
return apiService.untagUser({screen_name, tag, credentials})
|
||||
}
|
||||
|
||||
const addRight = ({screen_name}, right) => {
|
||||
return apiService.addRight({screen_name, right, credentials})
|
||||
}
|
||||
|
||||
const deleteRight = ({screen_name}, right) => {
|
||||
return apiService.deleteRight({screen_name, right, credentials})
|
||||
}
|
||||
|
||||
const setActivationStatus = ({screen_name}, status) => {
|
||||
return apiService.setActivationStatus({screen_name, status, credentials})
|
||||
}
|
||||
|
||||
const deleteUser = ({screen_name}) => {
|
||||
return apiService.deleteUser({screen_name, credentials})
|
||||
}
|
||||
|
||||
const fetchMutes = () => apiService.fetchMutes({credentials})
|
||||
|
@ -97,13 +126,20 @@ const backendInteractorService = (credentials) => {
|
|||
fetchUserRelationship,
|
||||
fetchAllFollowing,
|
||||
verifyCredentials: apiService.verifyCredentials,
|
||||
startFetching,
|
||||
startFetchingTimeline,
|
||||
startFetchingNotifications,
|
||||
fetchMutes,
|
||||
muteUser,
|
||||
unmuteUser,
|
||||
fetchBlocks,
|
||||
fetchOAuthTokens,
|
||||
revokeOAuthToken,
|
||||
tagUser,
|
||||
untagUser,
|
||||
addRight,
|
||||
deleteRight,
|
||||
deleteUser,
|
||||
setActivationStatus,
|
||||
register,
|
||||
getCaptcha,
|
||||
updateAvatar,
|
||||
|
|
|
@ -39,7 +39,7 @@ export const parseUser = (data) => {
|
|||
return output
|
||||
}
|
||||
|
||||
// output.name = ??? missing
|
||||
output.name = data.display_name
|
||||
output.name_html = addEmojis(data.display_name, data.emojis)
|
||||
|
||||
// output.description = ??? missing
|
||||
|
@ -67,9 +67,14 @@ export const parseUser = (data) => {
|
|||
output.statusnet_blocking = relationship.blocking
|
||||
output.muted = relationship.muting
|
||||
}
|
||||
|
||||
output.rights = {
|
||||
moderator: data.pleroma.is_moderator,
|
||||
admin: data.pleroma.is_admin
|
||||
}
|
||||
}
|
||||
|
||||
// Missing, trying to recover
|
||||
// TODO: handle is_local
|
||||
output.is_local = !output.screen_name.includes('@')
|
||||
} else {
|
||||
output.screen_name = data.screen_name
|
||||
|
@ -103,7 +108,12 @@ export const parseUser = (data) => {
|
|||
|
||||
// QVITTER ONLY FOR NOW
|
||||
// Really only applies to logged in user, really.. I THINK
|
||||
output.rights = data.rights
|
||||
if (data.rights) {
|
||||
output.rights = {
|
||||
moderator: data.rights.delete_others_notice,
|
||||
admin: data.rights.admin
|
||||
}
|
||||
}
|
||||
output.no_rich_text = data.no_rich_text
|
||||
output.default_scope = data.default_scope
|
||||
output.hide_follows = data.hide_follows
|
||||
|
@ -119,12 +129,19 @@ export const parseUser = (data) => {
|
|||
output.locked = data.locked
|
||||
output.followers_count = data.followers_count
|
||||
output.statuses_count = data.statuses_count
|
||||
output.friends = []
|
||||
output.followers = []
|
||||
output.friendIds = []
|
||||
output.followerIds = []
|
||||
if (data.pleroma) {
|
||||
output.follow_request_count = data.pleroma.follow_request_count
|
||||
}
|
||||
|
||||
if (data.pleroma) {
|
||||
output.tags = data.pleroma.tags
|
||||
output.deactivated = data.pleroma.deactivated
|
||||
}
|
||||
|
||||
output.tags = output.tags || []
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
|
@ -172,28 +189,28 @@ export const parseStatus = (data) => {
|
|||
|
||||
output.statusnet_html = addEmojis(data.content, data.emojis)
|
||||
|
||||
// Not exactly the same but works?
|
||||
if (data.pleroma) {
|
||||
const { pleroma } = data
|
||||
output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content
|
||||
output.summary = pleroma.spoiler_text ? data.pleroma.spoiler_text['text/plain'] : data.spoiler_text
|
||||
output.statusnet_conversation_id = data.pleroma.conversation_id
|
||||
output.is_local = pleroma.local
|
||||
output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
|
||||
} else {
|
||||
output.text = data.content
|
||||
output.summary = data.spoiler_text
|
||||
}
|
||||
|
||||
output.in_reply_to_status_id = data.in_reply_to_id
|
||||
output.in_reply_to_user_id = data.in_reply_to_account_id
|
||||
output.replies_count = data.replies_count
|
||||
|
||||
// Missing!! fix in UI?
|
||||
// output.in_reply_to_screen_name = ???
|
||||
|
||||
// Not exactly the same but works
|
||||
output.statusnet_conversation_id = data.id
|
||||
|
||||
if (output.type === 'retweet') {
|
||||
output.retweeted_status = parseStatus(data.reblog)
|
||||
}
|
||||
|
||||
output.summary = data.spoiler_text
|
||||
output.summary_html = addEmojis(data.spoiler_text, data.emojis)
|
||||
output.external_url = data.url
|
||||
|
||||
// output.is_local = ??? missing
|
||||
} else {
|
||||
output.favorited = data.favorited
|
||||
output.fave_num = data.fave_num
|
||||
|
@ -221,7 +238,6 @@ export const parseStatus = (data) => {
|
|||
output.in_reply_to_status_id = data.in_reply_to_status_id
|
||||
output.in_reply_to_user_id = data.in_reply_to_user_id
|
||||
output.in_reply_to_screen_name = data.in_reply_to_screen_name
|
||||
|
||||
output.statusnet_conversation_id = data.statusnet_conversation_id
|
||||
|
||||
if (output.type === 'retweet') {
|
||||
|
@ -272,9 +288,11 @@ export const parseNotification = (data) => {
|
|||
|
||||
if (masto) {
|
||||
output.type = mastoDict[data.type] || data.type
|
||||
// output.seen = ??? missing
|
||||
output.status = parseStatus(data.status)
|
||||
output.action = output.status // not sure
|
||||
output.seen = data.pleroma.is_seen
|
||||
output.status = output.type === 'follow'
|
||||
? null
|
||||
: parseStatus(data.status)
|
||||
output.action = output.status // TODO: Refactor, this is unneeded
|
||||
output.from_profile = parseUser(data.account)
|
||||
} else {
|
||||
const parsedNotice = parseStatus(data.notice)
|
||||
|
|
|
@ -23,18 +23,12 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => {
|
|||
|
||||
// For locked users we just mark it that we sent the follow request
|
||||
if (updated.locked) {
|
||||
resolve({
|
||||
sent: true,
|
||||
updated
|
||||
})
|
||||
resolve({ sent: true })
|
||||
}
|
||||
|
||||
if (updated.following) {
|
||||
// If we get result immediately, just stop.
|
||||
resolve({
|
||||
sent: false,
|
||||
updated
|
||||
})
|
||||
resolve({ sent: false })
|
||||
}
|
||||
|
||||
// But usually we don't get result immediately, so we ask server
|
||||
|
@ -48,16 +42,10 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => {
|
|||
.then((following) => {
|
||||
if (following) {
|
||||
// We confirmed and everything's good.
|
||||
resolve({
|
||||
sent: false,
|
||||
updated
|
||||
})
|
||||
resolve({ sent: false })
|
||||
} else {
|
||||
// If after all the tries, just treat it as if user is locked
|
||||
resolve({
|
||||
sent: false,
|
||||
updated
|
||||
})
|
||||
resolve({ sent: false })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -10,8 +10,8 @@ export const visibleTypes = store => ([
|
|||
].filter(_ => _))
|
||||
|
||||
const sortById = (a, b) => {
|
||||
const seqA = Number(a.action.id)
|
||||
const seqB = Number(b.action.id)
|
||||
const seqA = Number(a.id)
|
||||
const seqB = Number(b.id)
|
||||
const isSeqA = !Number.isNaN(seqA)
|
||||
const isSeqB = !Number.isNaN(seqB)
|
||||
if (isSeqA && isSeqB) {
|
||||
|
@ -21,7 +21,7 @@ const sortById = (a, b) => {
|
|||
} else if (!isSeqA && isSeqB) {
|
||||
return -1
|
||||
} else {
|
||||
return a.action.id > b.action.id ? -1 : 1
|
||||
return a.id > b.id ? -1 : 1
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,29 +11,35 @@ const fetchAndUpdate = ({store, credentials, older = false}) => {
|
|||
const rootState = store.rootState || store.state
|
||||
const timelineData = rootState.statuses.notifications
|
||||
|
||||
args['timeline'] = 'notifications'
|
||||
if (older) {
|
||||
if (timelineData.minId !== Number.POSITIVE_INFINITY) {
|
||||
args['until'] = timelineData.minId
|
||||
}
|
||||
return fetchNotifications({ store, args, older })
|
||||
} else {
|
||||
// load unread notifications repeadedly to provide consistency between browser tabs
|
||||
// fetch new notifications
|
||||
if (timelineData.maxId !== Number.POSITIVE_INFINITY) {
|
||||
args['since'] = timelineData.maxId
|
||||
}
|
||||
const result = fetchNotifications({ store, args, older })
|
||||
|
||||
// load unread notifications repeatedly to provide consistency between browser tabs
|
||||
const notifications = timelineData.data
|
||||
const unread = notifications.filter(n => !n.seen).map(n => n.id)
|
||||
if (!unread.length) {
|
||||
args['since'] = timelineData.maxId
|
||||
} else {
|
||||
args['since'] = Math.min(...unread) - 1
|
||||
if (timelineData.maxId !== Math.max(...unread)) {
|
||||
args['until'] = Math.max(...unread, args['since'] + 20)
|
||||
}
|
||||
}
|
||||
if (unread.length) {
|
||||
args['since'] = Math.min(...unread)
|
||||
fetchNotifications({ store, args, older })
|
||||
}
|
||||
|
||||
args['timeline'] = 'notifications'
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
const fetchNotifications = ({ store, args, older }) => {
|
||||
return apiService.fetchTimeline(args)
|
||||
.then((notifications) => {
|
||||
update({store, notifications, older})
|
||||
update({ store, notifications, older })
|
||||
return notifications
|
||||
}, () => store.dispatch('setNotificationsError', { value: true }))
|
||||
.catch(() => store.dispatch('setNotificationsError', { value: true }))
|
||||
|
|
|
@ -245,6 +245,12 @@
|
|||
"css": "bell-alt",
|
||||
"code": 61683,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "5bb103cd29de77e0e06a52638527b575",
|
||||
"css": "wrench",
|
||||
"code": 59418,
|
||||
"src": "fontawesome"
|
||||
}
|
||||
]
|
||||
}
|
2
static/font/css/fontello-codes.css
vendored
2
static/font/css/fontello-codes.css
vendored
|
@ -24,6 +24,8 @@
|
|||
.icon-adjust:before { content: '\e816'; } /* '' */
|
||||
.icon-edit:before { content: '\e817'; } /* '' */
|
||||
.icon-pencil:before { content: '\e818'; } /* '' */
|
||||
.icon-verified:before { content: '\e819'; } /* '' */
|
||||
.icon-wrench:before { content: '\e81a'; } /* '' */
|
||||
.icon-spin3:before { content: '\e832'; } /* '' */
|
||||
.icon-spin4:before { content: '\e834'; } /* '' */
|
||||
.icon-link-ext:before { content: '\f08e'; } /* '' */
|
||||
|
|
14
static/font/css/fontello-embedded.css
vendored
14
static/font/css/fontello-embedded.css
vendored
File diff suppressed because one or more lines are too long
2
static/font/css/fontello-ie7-codes.css
vendored
2
static/font/css/fontello-ie7-codes.css
vendored
|
@ -24,6 +24,8 @@
|
|||
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-verified { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
2
static/font/css/fontello-ie7.css
vendored
2
static/font/css/fontello-ie7.css
vendored
|
@ -35,6 +35,8 @@
|
|||
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-verified { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
16
static/font/css/fontello.css
vendored
16
static/font/css/fontello.css
vendored
|
@ -1,11 +1,11 @@
|
|||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('../font/fontello.eot?72648396');
|
||||
src: url('../font/fontello.eot?72648396#iefix') format('embedded-opentype'),
|
||||
url('../font/fontello.woff2?72648396') format('woff2'),
|
||||
url('../font/fontello.woff?72648396') format('woff'),
|
||||
url('../font/fontello.ttf?72648396') format('truetype'),
|
||||
url('../font/fontello.svg?72648396#fontello') format('svg');
|
||||
src: url('../font/fontello.eot?11878820');
|
||||
src: url('../font/fontello.eot?11878820#iefix') format('embedded-opentype'),
|
||||
url('../font/fontello.woff2?11878820') format('woff2'),
|
||||
url('../font/fontello.woff?11878820') format('woff'),
|
||||
url('../font/fontello.ttf?11878820') format('truetype'),
|
||||
url('../font/fontello.svg?11878820#fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
|||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('../font/fontello.svg?72648396#fontello') format('svg');
|
||||
src: url('../font/fontello.svg?11878820#fontello') format('svg');
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@ -80,6 +80,8 @@
|
|||
.icon-adjust:before { content: '\e816'; } /* '' */
|
||||
.icon-edit:before { content: '\e817'; } /* '' */
|
||||
.icon-pencil:before { content: '\e818'; } /* '' */
|
||||
.icon-verified:before { content: '\e819'; } /* '' */
|
||||
.icon-wrench:before { content: '\e81a'; } /* '' */
|
||||
.icon-spin3:before { content: '\e832'; } /* '' */
|
||||
.icon-spin4:before { content: '\e834'; } /* '' */
|
||||
.icon-link-ext:before { content: '\f08e'; } /* '' */
|
||||
|
|
26
static/font/demo.html
Normal file → Executable file
26
static/font/demo.html
Normal file → Executable file
|
@ -229,11 +229,11 @@ body {
|
|||
}
|
||||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('./font/fontello.eot?23081587');
|
||||
src: url('./font/fontello.eot?23081587#iefix') format('embedded-opentype'),
|
||||
url('./font/fontello.woff?23081587') format('woff'),
|
||||
url('./font/fontello.ttf?23081587') format('truetype'),
|
||||
url('./font/fontello.svg?23081587#fontello') format('svg');
|
||||
src: url('./font/fontello.eot?60799712');
|
||||
src: url('./font/fontello.eot?60799712#iefix') format('embedded-opentype'),
|
||||
url('./font/fontello.woff?60799712') format('woff'),
|
||||
url('./font/fontello.ttf?60799712') format('truetype'),
|
||||
url('./font/fontello.svg?60799712#fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -335,25 +335,29 @@ body {
|
|||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xe818"><i class="demo-icon icon-pencil"></i> <span class="i-name">icon-pencil</span><span class="i-code">0xe818</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe819"><i class="demo-icon icon-verified"></i> <span class="i-name">icon-verified</span><span class="i-code">0xe819</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe81a"><i class="demo-icon icon-wrench"></i> <span class="i-name">icon-wrench</span><span class="i-code">0xe81a</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin"></i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin"></i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext"></i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt"></i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu"></i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt"></i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty"></i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf0f3"><i class="demo-icon icon-bell-alt"></i> <span class="i-name">icon-bell-alt</span><span class="i-code">0xf0f3</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared"></i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply"></i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt"></i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply"></i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt"></i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf144"><i class="demo-icon icon-play-circled"></i> <span class="i-name">icon-play-circled</span><span class="i-code">0xf144</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt"></i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars"></i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus"></i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div>
|
||||
</div>
|
||||
|
|
Binary file not shown.
|
@ -56,6 +56,10 @@
|
|||
|
||||
<glyph glyph-name="pencil" unicode="" d="M203 0l50 51-131 131-51-51v-60h72v-71h60z m291 518q0 12-12 12-5 0-9-4l-303-302q-4-4-4-10 0-12 13-12 5 0 9 4l303 302q3 4 3 10z m-30 107l232-232-464-465h-232v233z m381-54q0-29-20-50l-93-93-232 233 93 92q20 21 50 21 29 0 51-21l131-131q20-22 20-51z" horiz-adv-x="857.1" />
|
||||
|
||||
<glyph glyph-name="verified" unicode="" d="M926 453l-19 13c-21 14-30 41-23 65l6 22c10 34-13 69-48 75l-23 4c-25 4-45 23-49 48l-4 23c-6 35-41 57-75 47l-22-7c-24-7-51 2-65 22l-14 20c-21 29-62 33-88 9l-17-16c-19-17-46-21-69-8l-20 11c-31 17-70 3-84-30l-9-22c-9-24-33-39-58-37l-23 1c-36 2-65-28-62-63l2-23c2-25-13-49-36-59l-21-9c-33-14-46-53-29-84l12-20c13-22 10-50-7-69l-15-17c-24-27-19-68 11-88l19-13c21-14 30-41 23-65l-9-23c-10-34 13-69 48-75l23-4c25-4 45-23 49-48l4-23c6-35 41-57 75-47l22 7c24 7 51-2 65-22l14-19c21-29 62-33 88-9l17 16c19 17 46 21 69 8l20-11c31-17 70-3 84 30l9 22c9 24 33 39 58 37l23-1c36-2 65 28 62 63l-1 23c-2 25 13 49 36 59l21 9c33 14 46 53 29 84l-12 20c-13 22-10 50 7 69l15 17c25 26 20 68-9 88z m-399-189l-82-81-81 82-78 79 82 81 78-79 187 186 81-82-187-186z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="wrench" unicode="" d="M214 36q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m360 234l-381-381q-21-20-50-20-29 0-51 20l-59 61q-21 20-21 50 0 29 21 51l380 380q22-55 64-97t97-64z m354 243q0-22-13-59-27-75-92-122t-144-46q-104 0-177 73t-73 177 73 176 177 74q32 0 67-10t60-26q9-6 9-15t-9-16l-163-94v-125l108-60q2 2 44 27t75 45 40 20q8 0 13-5t5-14z" horiz-adv-x="928.6" />
|
||||
|
||||
<glyph glyph-name="spin3" unicode="" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="spin4" unicode="" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
|
||||
|
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 20 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
125
test/fixtures/mastoapi.json
vendored
125
test/fixtures/mastoapi.json
vendored
|
@ -58,7 +58,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639",
|
||||
"url": "https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -127,7 +130,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/6634d32b-96a8-4852-a3db-ac8730715779",
|
||||
"url": "https://shigusegubu.club/objects/6634d32b-96a8-4852-a3db-ac8730715779",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -250,7 +256,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b",
|
||||
"url": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
},
|
||||
"reblogged": false,
|
||||
"reblogs_count": 0,
|
||||
|
@ -260,7 +269,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b",
|
||||
"url": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -329,7 +341,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/0f963ca1-a263-41ca-a43c-b5d26d0a08e9",
|
||||
"url": "https://shigusegubu.club/objects/0f963ca1-a263-41ca-a43c-b5d26d0a08e9",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -390,7 +405,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/3f809bd8-656f-4a29-81d4-80eed6916eb0",
|
||||
"url": "https://shigusegubu.club/objects/3f809bd8-656f-4a29-81d4-80eed6916eb0",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -516,7 +534,10 @@
|
|||
}],
|
||||
"uri": "tag:social.super-niche.club,2019-01-17:noticeId=2353002:objectType=note",
|
||||
"url": "https://social.super-niche.club/notice/2353002",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
},
|
||||
"reblogged": false,
|
||||
"reblogs_count": 0,
|
||||
|
@ -529,7 +550,10 @@
|
|||
}],
|
||||
"uri": "tag:social.super-niche.club,2019-01-17:noticeId=2353002:objectType=note",
|
||||
"url": "tag:social.super-niche.club,2019-01-17:noticeId=2353002:objectType=note",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -657,7 +681,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab",
|
||||
"url": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
},
|
||||
"reblogged": false,
|
||||
"reblogs_count": 0,
|
||||
|
@ -667,7 +694,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab",
|
||||
"url": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -733,7 +763,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/38b1bc44-15d8-40dd-b1aa-937e0ff4a86d",
|
||||
"url": "https://shigusegubu.club/objects/38b1bc44-15d8-40dd-b1aa-937e0ff4a86d",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -794,7 +827,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/fbff5da4-a517-42a9-bca9-17cae8cf2542",
|
||||
"url": "https://shigusegubu.club/objects/fbff5da4-a517-42a9-bca9-17cae8cf2542",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -850,7 +886,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/4007d659-27c6-4577-be10-fd134f5e4e7e",
|
||||
"url": "https://shigusegubu.club/objects/4007d659-27c6-4577-be10-fd134f5e4e7e",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -906,7 +945,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/59912d51-1cc6-4dc7-828c-f167e6c8b391",
|
||||
"url": "https://shigusegubu.club/objects/59912d51-1cc6-4dc7-828c-f167e6c8b391",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -962,7 +1004,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/62690bce-3f49-4047-9c8e-8941f2f79e10",
|
||||
"url": "https://shigusegubu.club/objects/62690bce-3f49-4047-9c8e-8941f2f79e10",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -1023,7 +1068,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/818f3dd0-2ff8-4def-a170-e4d4c405f387",
|
||||
"url": "https://shigusegubu.club/objects/818f3dd0-2ff8-4def-a170-e4d4c405f387",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -1084,7 +1132,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/0783a193-c097-488d-8944-47df9372cd6e",
|
||||
"url": "https://shigusegubu.club/objects/0783a193-c097-488d-8944-47df9372cd6e",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -1145,7 +1196,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/145d5252-7b8e-467d-9f36-1db0818f452f",
|
||||
"url": "https://shigusegubu.club/objects/145d5252-7b8e-467d-9f36-1db0818f452f",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -1252,7 +1306,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a",
|
||||
"url": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
},
|
||||
"reblogged": false,
|
||||
"reblogs_count": 0,
|
||||
|
@ -1262,7 +1319,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a",
|
||||
"url": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -1323,7 +1383,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/d4eb7c46-02f9-4b1f-83af-926cefa21f33",
|
||||
"url": "https://shigusegubu.club/objects/d4eb7c46-02f9-4b1f-83af-926cefa21f33",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -1446,7 +1509,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733",
|
||||
"url": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
},
|
||||
"reblogged": false,
|
||||
"reblogs_count": 0,
|
||||
|
@ -1456,7 +1522,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733",
|
||||
"url": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -1517,7 +1586,10 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/f472f4ed-8b0b-492f-9d53-d69eda79629d",
|
||||
"url": "https://shigusegubu.club/objects/f472f4ed-8b0b-492f-9d53-d69eda79629d",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}, {
|
||||
"account": {
|
||||
"acct": "hj",
|
||||
|
@ -1578,5 +1650,8 @@
|
|||
"tags": [],
|
||||
"uri": "https://shigusegubu.club/objects/d6fb4fd2-1f6a-4446-a1a6-5edd34050096",
|
||||
"url": "https://shigusegubu.club/objects/d6fb4fd2-1f6a-4446-a1a6-5edd34050096",
|
||||
"visibility": "public"
|
||||
"visibility": "public",
|
||||
"pleroma": {
|
||||
"local": true
|
||||
}
|
||||
}]
|
||||
|
|
|
@ -129,7 +129,10 @@ const makeMockStatusMasto = (overrides = {}) => {
|
|||
tags: [],
|
||||
uri: 'https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639',
|
||||
url: 'https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639',
|
||||
visibility: 'public'
|
||||
visibility: 'public',
|
||||
pleroma: {
|
||||
local: true
|
||||
}
|
||||
}, overrides)
|
||||
}
|
||||
|
||||
|
|
|
@ -9,14 +9,17 @@ describe('NotificationUtils', () => {
|
|||
notifications: {
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
action: { id: '1' },
|
||||
type: 'like'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
action: { id: '2' },
|
||||
type: 'mention'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
action: { id: '3' },
|
||||
type: 'repeat'
|
||||
}
|
||||
|
@ -35,10 +38,12 @@ describe('NotificationUtils', () => {
|
|||
const expected = [
|
||||
{
|
||||
action: { id: '3' },
|
||||
id: 3,
|
||||
type: 'repeat'
|
||||
},
|
||||
{
|
||||
action: { id: '1' },
|
||||
id: 1,
|
||||
type: 'like'
|
||||
}
|
||||
]
|
||||
|
|
18
yarn.lock
18
yarn.lock
|
@ -6097,6 +6097,10 @@ pkg-dir@^3.0.0:
|
|||
dependencies:
|
||||
find-up "^3.0.0"
|
||||
|
||||
popper.js@^1.14.7:
|
||||
version "1.14.7"
|
||||
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e"
|
||||
|
||||
posix-character-classes@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
|
||||
|
@ -8068,6 +8072,10 @@ uuid@^3.3.2:
|
|||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
|
||||
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
|
||||
|
||||
v-click-outside@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-2.1.1.tgz#5af80b68a1c82eac89c597890434fa3994b42ed1"
|
||||
|
||||
validate-npm-package-license@^3.0.1:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
|
||||
|
@ -8141,6 +8149,10 @@ vue-hot-reload-api@^2.2.0:
|
|||
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz#2756f46cb3258054c5f4723de8ae7e87302a1ccf"
|
||||
integrity sha512-KmvZVtmM26BQOMK1rwUZsrqxEGeKiYSZGA7SNWE6uExx8UX/cj9hq2MRV/wWC3Cq6AoeDGk57rL9YMFRel/q+g==
|
||||
|
||||
vue-hot-reload-api@^2.0.11:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.1.tgz#b2d3d95402a811602380783ea4f566eb875569a2"
|
||||
|
||||
vue-i18n@^7.3.2:
|
||||
version "7.8.1"
|
||||
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-7.8.1.tgz#2ce4b6efde679a1e05ddb5d907bfc1bc218803b2"
|
||||
|
@ -8165,6 +8177,12 @@ vue-loader@^14.0.0:
|
|||
vue-style-loader "^4.0.1"
|
||||
vue-template-es2015-compiler "^1.6.0"
|
||||
|
||||
vue-popperjs@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-popperjs/-/vue-popperjs-2.0.3.tgz#7c446d0ba7c63170ccb33a02669d0df4efc3d8cd"
|
||||
dependencies:
|
||||
popper.js "^1.14.7"
|
||||
|
||||
vue-router@^3.0.1:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be"
|
||||
|
|
Loading…
Reference in a new issue