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:
Henry Jameson 2019-04-27 12:26:17 +03:00
commit e0247e21f6
76 changed files with 1859 additions and 513 deletions

View file

@ -24,11 +24,13 @@
"localforage": "^1.5.0", "localforage": "^1.5.0",
"object-path": "^0.11.3", "object-path": "^0.11.3",
"phoenix": "^1.3.0", "phoenix": "^1.3.0",
"popper.js": "^1.14.7",
"sanitize-html": "^1.13.0", "sanitize-html": "^1.13.0",
"v-click-outside": "^2.1.1",
"vue": "^2.5.13", "vue": "^2.5.13",
"vue-chat-scroll": "^1.2.1", "vue-chat-scroll": "^1.2.1",
"vue-compose": "^0.7.1",
"vue-i18n": "^7.3.2", "vue-i18n": "^7.3.2",
"vue-popperjs": "^2.0.3",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-template-compiler": "^2.3.4", "vue-template-compiler": "^2.3.4",
"vue-timeago": "^3.1.2", "vue-timeago": "^3.1.2",

View file

@ -101,6 +101,14 @@ button {
background-color: $fallback--bg; background-color: $fallback--bg;
background-color: var(--bg, $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 { label.select {

View file

@ -171,9 +171,10 @@ const getCustomEmoji = async ({ store }) => {
try { try {
const res = await window.fetch('/api/pleroma/emoji.json') const res = await window.fetch('/api/pleroma/emoji.json')
if (res.ok) { 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) => { 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: 'customEmoji', value: emoji })
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true }) store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true })
@ -211,6 +212,7 @@ const getNodeInfo = async ({ store }) => {
const frontendVersion = window.___pleromafe_commit_hash const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion }) store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
} else { } else {
throw (res) throw (res)
} }

View 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
}
}
}

View 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>

View file

@ -24,19 +24,11 @@
<script src="./basic_user_card.js"></script> <script src="./basic_user_card.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss';
.basic-user-card { .basic-user-card {
display: flex; display: flex;
flex: 1 0; flex: 1 0;
margin: 0; margin: 0;
padding-top: 0.6em; padding: 0.6em 1em;
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);
&-collapsed-content { &-collapsed-content {
margin-left: 0.7em; margin-left: 0.7em;

View 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>

View file

@ -1,5 +1,4 @@
import { reduce, filter, findIndex } from 'lodash' import { reduce, filter, findIndex, clone } from 'lodash'
import { set } from 'vue'
import Status from '../status/status.vue' import Status from '../status/status.vue'
const sortById = (a, b) => { const sortById = (a, b) => {
@ -36,8 +35,7 @@ const conversation = {
data () { data () {
return { return {
highlight: null, highlight: null,
expanded: false, expanded: false
converationStatusIds: []
} }
}, },
props: [ props: [
@ -54,15 +52,6 @@ const conversation = {
status () { status () {
return this.statusoid return this.statusoid
}, },
idsToShow () {
if (this.converationStatusIds.length > 0) {
return this.converationStatusIds
} else if (this.statusId) {
return [this.statusId]
} else {
return []
}
},
statusId () { statusId () {
if (this.statusoid.retweeted_status) { if (this.statusoid.retweeted_status) {
return this.statusoid.retweeted_status.id return this.statusoid.retweeted_status.id
@ -70,6 +59,13 @@ const conversation = {
return this.statusoid.id 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 () { conversation () {
if (!this.status) { if (!this.status) {
return [] return []
@ -79,12 +75,7 @@ const conversation = {
return [this.status] return [this.status]
} }
const statusesObject = this.$store.state.statuses.allStatusesObject const conversation = clone(this.$store.state.statuses.conversationsObject[this.conversationId])
const conversation = this.idsToShow.reduce((acc, id) => {
acc.push(statusesObject[id])
return acc
}, [])
const statusIndex = findIndex(conversation, { id: this.statusId }) const statusIndex = findIndex(conversation, { id: this.statusId })
if (statusIndex !== -1) { if (statusIndex !== -1) {
conversation[statusIndex] = this.status conversation[statusIndex] = this.status
@ -131,10 +122,6 @@ const conversation = {
.then(({ancestors, descendants}) => { .then(({ancestors, descendants}) => {
this.$store.dispatch('addNewStatuses', { statuses: ancestors }) this.$store.dispatch('addNewStatuses', { statuses: ancestors })
this.$store.dispatch('addNewStatuses', { statuses: descendants }) 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)) .then(() => this.setHighlight(this.statusId))
} else { } else {

View file

@ -13,7 +13,7 @@
:key="status.id" :key="status.id"
:inlineExpanded="collapsable" :inlineExpanded="collapsable"
:statusoid="status" :statusoid="status"
:expandable='!expanded' :expandable='!isExpanded'
:focused="focused(status.id)" :focused="focused(status.id)"
:inConversation="isExpanded" :inConversation="isExpanded"
:highlight="getHighlight()" :highlight="getHighlight()"

View file

@ -10,7 +10,11 @@ const DeleteButton = {
}, },
computed: { computed: {
currentUser () { return this.$store.state.users.currentUser }, 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
}
} }
} }

View file

@ -0,0 +1,14 @@
const DialogModal = {
props: {
darkOverlay: {
default: true,
type: Boolean
},
onCancel: {
default: () => {},
type: Function
}
}
}
export default DialogModal

View 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>

View file

@ -10,8 +10,7 @@ const FollowCard = {
data () { data () {
return { return {
inProgress: false, inProgress: false,
requestSent: false, requestSent: false
updated: false
} }
}, },
components: { components: {
@ -19,10 +18,8 @@ const FollowCard = {
RemoteFollow RemoteFollow
}, },
computed: { computed: {
isMe () { return this.$store.state.users.currentUser.id === this.user.id }, isMe () {
following () { return this.updated ? this.updated.following : this.user.following }, return this.$store.state.users.currentUser.id === this.user.id
showFollow () {
return !this.following || this.updated && !this.updated.following
}, },
loggedIn () { loggedIn () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
@ -31,17 +28,15 @@ const FollowCard = {
methods: { methods: {
followUser () { followUser () {
this.inProgress = true this.inProgress = true
requestFollow(this.user, this.$store).then(({ sent, updated }) => { requestFollow(this.user, this.$store).then(({ sent }) => {
this.inProgress = false this.inProgress = false
this.requestSent = sent this.requestSent = sent
this.updated = updated
}) })
}, },
unfollowUser () { unfollowUser () {
this.inProgress = true this.inProgress = true
requestUnfollow(this.user, this.$store).then(({ updated }) => { requestUnfollow(this.user, this.$store).then(() => {
this.inProgress = false this.inProgress = false
this.updated = updated
}) })
} }
} }

View file

@ -4,11 +4,14 @@
<span class="faint" v-if="!noFollowsYou && user.follows_you"> <span class="faint" v-if="!noFollowsYou && user.follows_you">
{{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }} {{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }}
</span> </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" /> <RemoteFollow :user="user" />
</div> </div>
</template>
<template v-else>
<button <button
v-if="showFollow && loggedIn" v-if="!user.following"
class="btn btn-default follow-card-follow-button" class="btn btn-default follow-card-follow-button"
@click="followUser" @click="followUser"
:disabled="inProgress" :disabled="inProgress"
@ -24,7 +27,7 @@
{{ $t('user_card.follow') }} {{ $t('user_card.follow') }}
</template> </template>
</button> </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"> <template v-if="inProgress">
{{ $t('user_card.follow_progress') }} {{ $t('user_card.follow_progress') }}
</template> </template>
@ -32,6 +35,7 @@
{{ $t('user_card.follow_unfollow') }} {{ $t('user_card.follow_unfollow') }}
</template> </template>
</button> </button>
</template>
</div> </div>
</basic-user-card> </basic-user-card>
</template> </template>

View file

@ -4,7 +4,7 @@
{{$t('nav.friend_requests')}} {{$t('nav.friend_requests')}}
</div> </div>
<div class="panel-body"> <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>
</div> </div>
</template> </template>

View 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>

View file

@ -31,15 +31,19 @@ const LoginForm = {
username: this.user.username, username: this.user.username,
password: this.user.password password: this.user.password
} }
).then((result) => { ).then(async (result) => {
if (result.error) { if (result.error) {
this.authError = result.error this.authError = result.error
this.user.password = '' this.user.password = ''
return return
} }
this.$store.commit('setToken', result.access_token) 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'}) this.$router.push({name: 'friends'})
} catch (e) {
console.log(e)
}
}) })
}) })
}, },

View 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

View 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>

View file

@ -21,25 +21,28 @@ const Notification = {
}, },
userProfileLink (user) { userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) 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: { computed: {
userClass () { userClass () {
return highlightClass(this.notification.action.user) return highlightClass(this.notification.from_profile)
}, },
userStyle () { userStyle () {
const highlight = this.$store.state.config.highlight 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]) return highlightStyle(highlight[user.screen_name])
}, },
userInStore () { userInStore () {
return this.$store.getters.findUser(this.notification.action.user.id) return this.$store.getters.findUser(this.notification.from_profile.id)
}, },
user () { user () {
if (this.userInStore) { if (this.userInStore) {
return this.userInStore return this.userInStore
} }
return {} return this.notification.from_profile
} }
} }
} }

View file

@ -1,15 +1,20 @@
<template> <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> <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"> <a class='avatar-container' :href="notification.from_profile.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
<UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/> <UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.from_profile.profile_image_url_original" />
</a> </a>
<div class='notification-right'> <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"> <span class="notification-details">
<div class="name-and-action"> <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-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.action.user.screen_name">{{ notification.action.user.name }}</span> <span class="username" v-else :title="'@'+notification.from_profile.screen_name">{{ notification.from_profile.name }}</span>
<span v-if="notification.type === 'like'"> <span v-if="notification.type === 'like'">
<i class="fa icon-star lit"></i> <i class="fa icon-star lit"></i>
<small>{{$t('notifications.favorited_you')}}</small> <small>{{$t('notifications.favorited_you')}}</small>
@ -23,19 +28,24 @@
<small>{{$t('notifications.followed_you')}}</small> <small>{{$t('notifications.followed_you')}}</small>
</span> </span>
</div> </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"> <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> </router-link>
</div> </div>
</span> </span>
<div class="follow-text" v-if="notification.type === 'follow'"> <div class="follow-text" v-if="notification.type === 'follow'">
<router-link :to="userProfileLink(notification.action.user)"> <router-link :to="userProfileLink(notification.from_profile)">
@{{notification.action.user.screen_name}} @{{notification.from_profile.screen_name}}
</router-link> </router-link>
</div> </div>
<template v-else> <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> </template>
</div> </div>
</div> </div>

View file

@ -10,13 +10,6 @@ const Notifications = {
props: [ props: [
'noHeading' '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 () { data () {
return { return {
bottomedOut: false bottomedOut: false
@ -56,7 +49,7 @@ const Notifications = {
}, },
methods: { methods: {
markAsSeen () { markAsSeen () {
this.$store.dispatch('markNotificationsAsSeen', this.visibleNotifications) this.$store.dispatch('markNotificationsAsSeen')
}, },
fetchOlderNotifications () { fetchOlderNotifications () {
const store = this.$store const store = this.$store

View file

@ -12,7 +12,7 @@
<button v-if="unseenCount" @click.prevent="markAsSeen" class="read-button">{{$t('notifications.read')}}</button> <button v-if="unseenCount" @click.prevent="markAsSeen" class="read-button">{{$t('notifications.read')}}</button>
</div> </div>
<div class="panel-body"> <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> <div class="notification-overlay"></div>
<notification :notification="notification"></notification> <notification :notification="notification"></notification>
</div> </div>

View 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;
}

View 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>

View file

@ -7,7 +7,7 @@ const PublicAndExternalTimeline = {
timeline () { return this.$store.state.statuses.timelines.publicAndExternal } timeline () { return this.$store.state.statuses.timelines.publicAndExternal }
}, },
created () { created () {
this.$store.dispatch('startFetching', { timeline: 'publicAndExternal' }) this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' })
}, },
destroyed () { destroyed () {
this.$store.dispatch('stopFetching', 'publicAndExternal') this.$store.dispatch('stopFetching', 'publicAndExternal')

View file

@ -7,7 +7,7 @@ const PublicTimeline = {
timeline () { return this.$store.state.statuses.timelines.public } timeline () { return this.$store.state.statuses.timelines.public }
}, },
created () { created () {
this.$store.dispatch('startFetching', { timeline: 'public' }) this.$store.dispatch('startFetchingTimeline', { timeline: 'public' })
}, },
destroyed () { destroyed () {
this.$store.dispatch('stopFetching', 'public') this.$store.dispatch('stopFetching', 'public')

View 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

View 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>

View file

@ -42,9 +42,7 @@
</li> </li>
<li> <li>
<input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal"> <input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal">
<label for="collapseMessageWithSubject"> <label for="collapseMessageWithSubject">{{$t('settings.collapse_subject')}} {{$t('settings.instance_default', { value: collapseMessageWithSubjectDefault })}}</label>
{{$t('settings.collapse_subject')}} {{$t('settings.instance_default', { value: collapseMessageWithSubjectDefault })}}
</label>
</li> </li>
<li> <li>
<input type="checkbox" id="streaming" v-model="streamingLocal"> <input type="checkbox" id="streaming" v-model="streamingLocal">
@ -330,6 +328,7 @@
textarea { textarea {
width: 100%; width: 100%;
max-width: 100%;
height: 100px; height: 100px;
} }

View file

@ -3,7 +3,7 @@ import Timeline from '../timeline/timeline.vue'
const TagTimeline = { const TagTimeline = {
created () { created () {
this.$store.commit('clearTimeline', { timeline: '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 })
}, },
components: { components: {
Timeline Timeline
@ -15,7 +15,7 @@ const TagTimeline = {
watch: { watch: {
tag () { tag () {
this.$store.commit('clearTimeline', { timeline: '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 () { destroyed () {

View file

@ -52,7 +52,7 @@ const Timeline = {
window.addEventListener('scroll', this.scrollLoad) window.addEventListener('scroll', this.scrollLoad)
if (this.timelineName === 'friends' && !credentials) { return false } if (store.state.api.fetchers[this.timelineName]) { return false }
timelineFetcher.fetchAndUpdate({ timelineFetcher.fetchAndUpdate({
store, store,

View file

@ -1,5 +1,6 @@
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.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 { hex2rgb } from '../../services/color_convert/color_convert.js'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -93,15 +94,17 @@ export default {
} }
}, },
visibleRole () { visibleRole () {
const validRole = (this.user.role === 'admin' || this.user.role === 'moderator') const rights = this.user.rights
const showRole = this.isOtherUser || this.user.show_role if (!rights) { return }
const validRole = rights.admin || rights.moderator
return validRole && showRole && this.user.role const roleTitle = rights.admin ? 'admin' : 'moderator'
return validRole && roleTitle
} }
}, },
components: { components: {
UserAvatar, UserAvatar,
RemoteFollow RemoteFollow,
ModerationTools
}, },
methods: { methods: {
followUser () { followUser () {

View file

@ -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-if="user.name_html" v-html="user.name_html"></div>
<div :title="user.name" class='user-name' v-else>{{user.name}}</div> <div :title="user.name" class='user-name' v-else>{{user.name}}</div>
<router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser"> <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> </router-link>
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local"> <a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local">
<i class="icon-link-ext usersettings"></i> <i class="icon-link-ext usersettings"></i>
@ -99,6 +99,8 @@
</button> </button>
</span> </span>
</div> </div>
<ModerationTools :user='user' v-if='loggedIn.role === "admin"'>
</ModerationTools>
</div> </div>
</div> </div>
</div> </div>
@ -160,7 +162,7 @@
max-width: 100%; max-width: 100%;
max-height: 400px; max-height: 400px;
.emoji { &.emoji {
width: 32px; width: 32px;
height: 32px; height: 32px;
} }

View file

@ -1,47 +1,37 @@
import { compose } from 'vue-compose'
import get from 'lodash/get' import get from 'lodash/get'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import FollowCard from '../follow_card/follow_card.vue' import FollowCard from '../follow_card/follow_card.vue'
import Timeline from '../timeline/timeline.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 withLoadMore from '../../hocs/with_load_more/with_load_more'
import withList from '../../hocs/with_list/with_list'
const FollowerList = compose( const FollowerList = withLoadMore({
withLoadMore({ fetch: (props, $store) => $store.dispatch('fetchFollowers', props.userId),
fetch: (props, $store) => $store.dispatch('addFollowers', props.userId), select: (props, $store) => get($store.getters.findUser(props.userId), 'followerIds', []).map(id => $store.getters.findUser(id)),
select: (props, $store) => get($store.getters.findUser(props.userId), 'followers', []), destroy: (props, $store) => $store.dispatch('clearFollowers', props.userId),
destory: (props, $store) => $store.dispatch('clearFollowers', props.userId), childPropName: 'items',
childPropName: 'entries',
additionalPropNames: ['userId'] additionalPropNames: ['userId']
}), })(List)
withList({ getEntryProps: user => ({ user }) })
)(FollowCard)
const FriendList = compose( const FriendList = withLoadMore({
withLoadMore({ fetch: (props, $store) => $store.dispatch('fetchFriends', props.userId),
fetch: (props, $store) => $store.dispatch('addFriends', props.userId), select: (props, $store) => get($store.getters.findUser(props.userId), 'friendIds', []).map(id => $store.getters.findUser(id)),
select: (props, $store) => get($store.getters.findUser(props.userId), 'friends', []), destroy: (props, $store) => $store.dispatch('clearFriends', props.userId),
destory: (props, $store) => $store.dispatch('clearFriends', props.userId), childPropName: 'items',
childPropName: 'entries',
additionalPropNames: ['userId'] additionalPropNames: ['userId']
}), })(List)
withList({ getEntryProps: user => ({ user }) })
)(FollowCard)
const UserProfile = { const UserProfile = {
data () { data () {
return { return {
error: false, error: false,
fetchedUserId: null userId: null
} }
}, },
created () { created () {
if (!this.user.id) { const routeParams = this.$route.params
this.fetchUserId() this.load(routeParams.name || routeParams.id)
.then(() => this.startUp())
} else {
this.startUp()
}
}, },
destroyed () { destroyed () {
this.cleanUp() this.cleanUp()
@ -56,26 +46,12 @@ const UserProfile = {
media () { media () {
return this.$store.state.statuses.timelines.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 () { isUs () {
return this.userId && this.$store.state.users.currentUser.id && return this.userId && this.$store.state.users.currentUser.id &&
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 () { user () {
if (this.userInStore) { return this.$store.getters.findUser(this.userId)
return this.userInStore
}
return {}
}, },
isExternal () { isExternal () {
return this.$route.name === 'external-user-profile' return this.$route.name === 'external-user-profile'
@ -88,22 +64,18 @@ const UserProfile = {
} }
}, },
methods: { methods: {
startFetchFavorites () { load (userNameOrId) {
if (this.isUs) { // Check if user data is already loaded in store
this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.userId }) const user = this.$store.getters.findUser(userNameOrId)
} if (user) {
}, this.userId = user.id
fetchUserId () { this.fetchTimelines()
let fetchPromise
if (this.userId && !this.$route.params.name) {
fetchPromise = this.$store.dispatch('fetchUser', this.userId)
} else { } else {
fetchPromise = this.$store.dispatch('fetchUser', this.userName) this.$store.dispatch('fetchUser', userNameOrId)
.then(({ id }) => { .then(({ id }) => {
this.fetchedUserId = id this.userId = id
this.fetchTimelines()
}) })
}
return fetchPromise
.catch((reason) => { .catch((reason) => {
const errorMessage = get(reason, 'error.error') const errorMessage = get(reason, 'error.error')
if (errorMessage === 'No user with such user_id') { // Known 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') this.error = this.$t('user_profile.profile_loading_error')
} }
}) })
.then(() => this.startUp()) }
}, },
startUp () { fetchTimelines () {
if (this.userId) { const userId = this.userId
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.userId }) this.$store.dispatch('startFetchingTimeline', { timeline: 'user', userId })
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.userId }) this.$store.dispatch('startFetchingTimeline', { timeline: 'media', userId })
this.startFetchFavorites() if (this.isUs) {
this.$store.dispatch('startFetchingTimeline', { timeline: 'favorites', userId })
} }
}, },
cleanUp () { cleanUp () {
@ -133,18 +106,16 @@ const UserProfile = {
} }
}, },
watch: { watch: {
// userId can be undefined if we don't know it yet '$route.params.id': function (newVal) {
userId (newVal) {
if (newVal) { if (newVal) {
this.cleanUp() this.cleanUp()
this.startUp() this.load(newVal)
} }
}, },
userName () { '$route.params.name': function (newVal) {
if (this.$route.params.name) { if (newVal) {
this.fetchUserId()
this.cleanUp() this.cleanUp()
this.startUp() this.load(newVal)
} }
}, },
$route () { $route () {
@ -155,7 +126,9 @@ const UserProfile = {
UserCard, UserCard,
Timeline, Timeline,
FollowerList, FollowerList,
FriendList FriendList,
ModerationTools,
FollowCard
} }
} }

View file

@ -1,6 +1,6 @@
<template> <template>
<div> <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"/> <UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/>
<tab-switcher :renderOnlyFocused="true" ref="tabSwitcher"> <tab-switcher :renderOnlyFocused="true" ref="tabSwitcher">
<Timeline <Timeline
@ -14,10 +14,18 @@
:user-id="userId" :user-id="userId"
/> />
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count"> <div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
<FriendList :userId="userId" /> <FriendList :userId="userId">
<template slot="item" slot-scope="{item}">
<FollowCard :user="item" />
</template>
</FriendList>
</div> </div>
<div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count"> <div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
<FollowerList :userId="userId" :entryProps="{noFollowsYou: isUs}" /> <FollowerList :userId="userId">
<template slot="item" slot-scope="{item}">
<FollowCard :user="item" :noFollowsYou="isUs" />
</template>
</FollowerList>
</div> </div>
<Timeline <Timeline
:label="$t('user_card.media')" :label="$t('user_card.media')"

View file

@ -1,5 +1,6 @@
import FollowCard from '../follow_card/follow_card.vue' import FollowCard from '../follow_card/follow_card.vue'
import userSearchApi from '../../services/new_api/user_search.js' import map from 'lodash/map'
const userSearch = { const userSearch = {
components: { components: {
FollowCard FollowCard
@ -10,10 +11,15 @@ const userSearch = {
data () { data () {
return { return {
username: '', username: '',
users: [], userIds: [],
loading: false loading: false
} }
}, },
computed: {
users () {
return this.userIds.map(userId => this.$store.getters.findUser(userId))
}
},
mounted () { mounted () {
this.search(this.query) this.search(this.query)
}, },
@ -33,10 +39,10 @@ const userSearch = {
return return
} }
this.loading = true this.loading = true
userSearchApi.search({query, store: this.$store}) this.$store.dispatch('searchUsers', query)
.then((res) => { .then((res) => {
this.loading = false this.loading = false
this.users = res this.userIds = map(res, 'id')
}) })
} }
} }

View file

@ -13,7 +13,7 @@
<i class="icon-spin3 animate-spin"/> <i class="icon-spin3 animate-spin"/>
</div> </div>
<div v-else class="panel-body"> <div v-else class="panel-body">
<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>
</div> </div>
</template> </template>

View file

@ -1,6 +1,7 @@
import { compose } from 'vue-compose'
import unescape from 'lodash/unescape' import unescape from 'lodash/unescape'
import get from 'lodash/get' import get from 'lodash/get'
import map from 'lodash/map'
import reject from 'lodash/reject'
import TabSwitcher from '../tab_switcher/tab_switcher.js' import TabSwitcher from '../tab_switcher/tab_switcher.js'
import ImageCropper from '../image_cropper/image_cropper.vue' import ImageCropper from '../image_cropper/image_cropper.vue'
import StyleSwitcher from '../style_switcher/style_switcher.vue' import StyleSwitcher from '../style_switcher/style_switcher.vue'
@ -8,27 +9,24 @@ import ScopeSelector from '../scope_selector/scope_selector.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js' import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import BlockCard from '../block_card/block_card.vue' import BlockCard from '../block_card/block_card.vue'
import MuteCard from '../mute_card/mute_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 EmojiInput from '../emoji-input/emoji-input.vue'
import Autosuggest from '../autosuggest/autosuggest.vue'
import withSubscription from '../../hocs/with_subscription/with_subscription' 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( const BlockList = withSubscription({
withSubscription({
fetch: (props, $store) => $store.dispatch('fetchBlocks'), fetch: (props, $store) => $store.dispatch('fetchBlocks'),
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []), select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
childPropName: 'entries' childPropName: 'items'
}), })(SelectableList)
withList({ getEntryProps: userId => ({ userId }) })
)(BlockCard)
const MuteList = compose( const MuteList = withSubscription({
withSubscription({
fetch: (props, $store) => $store.dispatch('fetchMutes'), fetch: (props, $store) => $store.dispatch('fetchMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []), select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
childPropName: 'entries' childPropName: 'items'
}), })(SelectableList)
withList({ getEntryProps: userId => ({ userId }) })
)(MuteCard)
const UserSettings = { const UserSettings = {
data () { data () {
@ -73,7 +71,11 @@ const UserSettings = {
ImageCropper, ImageCropper,
BlockList, BlockList,
MuteList, MuteList,
EmojiInput EmojiInput,
Autosuggest,
BlockCard,
MuteCard,
ProgressButton
}, },
computed: { computed: {
user () { user () {
@ -334,6 +336,40 @@ const UserSettings = {
if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) { if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
this.$store.dispatch('revokeToken', id) 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
} }
} }
} }

View file

@ -195,15 +195,51 @@
</div> </div>
<div :label="$t('settings.blocks_tab')"> <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> <template slot="empty">{{$t('settings.no_blocks')}}</template>
</block-list> </BlockList>
</div> </div>
<div :label="$t('settings.mutes_tab')"> <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> <template slot="empty">{{$t('settings.no_mutes')}}</template>
</mute-list> </MuteList>
</div> </div>
</tab-switcher> </tab-switcher>
</div> </div>
@ -262,5 +298,19 @@
text-align: right; text-align: right;
} }
} }
&-usersearch-wrapper {
padding: 1em;
}
&-bulk-actions {
text-align: right;
padding: 0 1em;
min-height: 28px;
button {
width: 10em;
}
}
} }
</style> </style>

View file

@ -4,7 +4,7 @@
{{$t('who_to_follow.who_to_follow')}} {{$t('who_to_follow.who_to_follow')}}
</div> </div>
<div class="panel-body"> <div class="panel-body">
<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>
</div> </div>
</template> </template>

View file

@ -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

View file

@ -1,6 +0,0 @@
.with-list {
&-empty-content {
text-align: center;
padding: 10px;
}
}

View file

@ -1,7 +1,13 @@
@import '../../_variables.scss';
.with-load-more { .with-load-more {
&-footer { &-footer {
padding: 10px; padding: 10px;
text-align: center; text-align: center;
border-top: 1px solid;
border-top-color: $fallback--border;
border-top-color: var(--border, $fallback--border);
.error { .error {
font-size: 14px; font-size: 14px;

View file

@ -73,7 +73,8 @@
"content_type": { "content_type": {
"text/plain": "Prostý text", "text/plain": "Prostý text",
"text/html": "HTML", "text/html": "HTML",
"text/markdown": "Markdown" "text/markdown": "Markdown",
"text/bbcode": "BBCode"
}, },
"content_warning": "Předmět (volitelný)", "content_warning": "Předmět (volitelný)",
"default": "Právě jsem přistál v L.A.", "default": "Právě jsem přistál v L.A.",

View file

@ -22,7 +22,8 @@
"generic_error": "An error occured", "generic_error": "An error occured",
"optional": "optional", "optional": "optional",
"show_more": "Show more", "show_more": "Show more",
"show_less": "Show less" "show_less": "Show less",
"cancel": "Cancel"
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "Crop picture", "crop_picture": "Crop picture",
@ -76,7 +77,8 @@
"content_type": { "content_type": {
"text/plain": "Plain text", "text/plain": "Plain text",
"text/html": "HTML", "text/html": "HTML",
"text/markdown": "Markdown" "text/markdown": "Markdown",
"text/bbcode": "BBCode"
}, },
"content_warning": "Subject (optional)", "content_warning": "Subject (optional)",
"default": "Just landed in L.A.", "default": "Just landed in L.A.",
@ -111,6 +113,9 @@
"password_confirmation_match": "should be the same as password" "password_confirmation_match": "should be the same as password"
} }
}, },
"selectable_list": {
"select_all": "Select all"
},
"settings": { "settings": {
"app_name": "App name", "app_name": "App name",
"attachmentRadius": "Attachments", "attachmentRadius": "Attachments",
@ -216,6 +221,8 @@
"reply_visibility_self": "Only show replies directed at me", "reply_visibility_self": "Only show replies directed at me",
"saving_err": "Error saving settings", "saving_err": "Error saving settings",
"saving_ok": "Settings saved", "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", "security_tab": "Security",
"scope_copy": "Copy scope when replying (DMs are always copied)", "scope_copy": "Copy scope when replying (DMs are always copied)",
"minimal_scopes_mode": "Minimize post scope selection options", "minimal_scopes_mode": "Minimize post scope selection options",
@ -403,7 +410,26 @@
"block_progress": "Blocking...", "block_progress": "Blocking...",
"unmute": "Unmute", "unmute": "Unmute",
"unmute_progress": "Unmuting...", "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": { "user_profile": {
"timeline_title": "User Timeline", "timeline_title": "User Timeline",

View file

@ -20,7 +20,10 @@
"submit": "Mandar", "submit": "Mandar",
"more": "Mai", "more": "Mai",
"generic_error": "Una error ses producha", "generic_error": "Una error ses producha",
"optional": "opcional" "optional": "opcional",
"show_more": "Mostrar mai",
"show_less": "Mostrar mens",
"cancel": "Anullar"
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "Talhar limatge", "crop_picture": "Talhar limatge",
@ -74,11 +77,13 @@
"content_type": { "content_type": {
"text/plain": "Tèxte brut", "text/plain": "Tèxte brut",
"text/html": "HTML", "text/html": "HTML",
"text/markdown": "Markdown" "text/markdown": "Markdown",
"text/bbcode": "BBCode"
}, },
"content_warning": "Avís de contengut (opcional)", "content_warning": "Avís de contengut (opcional)",
"default": "Escrivètz aquí vòstre estatut.", "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", "posting": "Mandadís",
"scope": { "scope": {
"direct": "Dirècte - Publicar pels utilizaires mencionats solament", "direct": "Dirècte - Publicar pels utilizaires mencionats solament",
@ -108,6 +113,9 @@
"password_confirmation_match": "deu èsser lo meteis senhal" "password_confirmation_match": "deu èsser lo meteis senhal"
} }
}, },
"selectable_list": {
"select_all": "O seleccionar tot"
},
"settings": { "settings": {
"app_name": "Nom de laplicacion", "app_name": "Nom de laplicacion",
"attachmentRadius": "Pèças juntas", "attachmentRadius": "Pèças juntas",
@ -213,8 +221,11 @@
"reply_visibility_self": "Mostrar pas que las responsas que me son destinadas", "reply_visibility_self": "Mostrar pas que las responsas que me son destinadas",
"saving_err": "Error en enregistrant los paramètres", "saving_err": "Error en enregistrant los paramètres",
"saving_ok": "Paramètres enregistrats", "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", "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 dopcions per publicacion",
"set_new_avatar": "Definir un nòu avatar", "set_new_avatar": "Definir un nòu avatar",
"set_new_profile_background": "Definir un nòu fons de perfil", "set_new_profile_background": "Definir un nòu fons de perfil",
"set_new_profile_banner": "Definir una nòva bandièra 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 dutilizacion", "checkbox": "Ai legit los tèrmes e condicions dutilizacion",
"link": "un pichon ligam simpatic" "link": "un pichon ligam simpatic"
} }
},
"version": {
"title": "Version",
"backend_version": "Version Backend",
"frontend_version": "Version Frontend"
} }
}, },
"timeline": { "timeline": {
@ -394,7 +410,26 @@
"block_progress": "Blocatge...", "block_progress": "Blocatge...",
"unmute": "Tornar mostrar", "unmute": "Tornar mostrar",
"unmute_progress": "Afichatge...", "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 dutilizaire dinstàncias alonhadas",
"disable_any_subscription": "Desactivar tot seguiment",
"quarantine": "Defendre la federacion de las publicacions de lutilizaire",
"delete_user": "Suprimir lutilizaire",
"delete_user_confirmation": "Volètz vertadièrament far aquò? Aquesta accion se pòt pas anullar."
}
}, },
"user_profile": { "user_profile": {
"timeline_title": "Flux utilizaire", "timeline_title": "Flux utilizaire",

View file

@ -74,7 +74,8 @@
"content_type": { "content_type": {
"text/plain": "Czysty tekst", "text/plain": "Czysty tekst",
"text/html": "HTML", "text/html": "HTML",
"text/markdown": "Markdown" "text/markdown": "Markdown",
"text/bbcode": "BBCode"
}, },
"content_warning": "Temat (nieobowiązkowy)", "content_warning": "Temat (nieobowiązkowy)",
"default": "Właśnie wróciłem z kościoła", "default": "Właśnie wróciłem z kościoła",

View file

@ -8,7 +8,8 @@
}, },
"general": { "general": {
"apply": "Применить", "apply": "Применить",
"submit": "Отправить" "submit": "Отправить",
"cancel": "Отмена"
}, },
"login": { "login": {
"login": "Войти", "login": "Войти",
@ -311,7 +312,26 @@
"muted": "Игнорирую", "muted": "Игнорирую",
"per_day": "в день", "per_day": "в день",
"remote_follow": "Читать удалённо", "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": { "user_profile": {
"timeline_title": "Лента пользователя" "timeline_title": "Лента пользователя"

View file

@ -22,6 +22,7 @@ import pushNotifications from './lib/push_notifications_plugin.js'
import messages from './i18n/messages.js' import messages from './i18n/messages.js'
import VueChatScroll from 'vue-chat-scroll' import VueChatScroll from 'vue-chat-scroll'
import VueClickOutside from 'v-click-outside'
import afterStoreSetup from './boot/after_store.js' import afterStoreSetup from './boot/after_store.js'
@ -39,6 +40,7 @@ Vue.use(VueTimeago, {
}) })
Vue.use(VueI18n) Vue.use(VueI18n)
Vue.use(VueChatScroll) Vue.use(VueChatScroll)
Vue.use(VueClickOutside)
const i18n = new VueI18n({ const i18n = new VueI18n({
// By default, use the browser locale, we will update it if neccessary // By default, use the browser locale, we will update it if neccessary
@ -59,6 +61,11 @@ const persistedStateOptions = {
const persistedState = await createPersistedState(persistedStateOptions) const persistedState = await createPersistedState(persistedStateOptions)
const store = new Vuex.Store({ const store = new Vuex.Store({
modules: { modules: {
i18n: {
getters: {
i18n: () => i18n
}
},
interface: interfaceModule, interface: interfaceModule,
instance: instanceModule, instance: instanceModule,
statuses: statusesModule, statuses: statusesModule,

View file

@ -13,11 +13,11 @@ const api = {
setBackendInteractor (state, backendInteractor) { setBackendInteractor (state, backendInteractor) {
state.backendInteractor = backendInteractor state.backendInteractor = backendInteractor
}, },
addFetcher (state, {timeline, fetcher}) { addFetcher (state, { fetcherName, fetcher }) {
state.fetchers[timeline] = fetcher state.fetchers[fetcherName] = fetcher
}, },
removeFetcher (state, {timeline}) { removeFetcher (state, { fetcherName }) {
delete state.fetchers[timeline] delete state.fetchers[fetcherName]
}, },
setWsToken (state, token) { setWsToken (state, token) {
state.wsToken = token state.wsToken = token
@ -33,17 +33,24 @@ const api = {
} }
}, },
actions: { 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. // Don't start fetching if we already are.
if (store.state.fetchers[timeline]) return if (store.state.fetchers[timeline]) return
const fetcher = store.state.backendInteractor.startFetching({ timeline, store, userId, tag }) const fetcher = store.state.backendInteractor.startFetchingTimeline({ timeline, store, userId, tag })
store.commit('addFetcher', { timeline, fetcher }) store.commit('addFetcher', { fetcherName: timeline, fetcher })
}, },
stopFetching (store, timeline) { startFetchingNotifications (store) {
const fetcher = store.state.fetchers[timeline] // 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) window.clearInterval(fetcher)
store.commit('removeFetcher', {timeline}) store.commit('removeFetcher', { fetcherName })
}, },
setWsToken (store, token) { setWsToken (store, token) {
store.commit('setWsToken', token) store.commit('setWsToken', token)

View file

@ -48,7 +48,6 @@ const interfaceMod = {
commit('setNotificationPermission', permission) commit('setNotificationPermission', permission)
}, },
setMobileLayout ({ commit }, value) { setMobileLayout ({ commit }, value) {
console.log('setMobileLayout called')
commit('setMobileLayout', value) commit('setMobileLayout', value)
} }
} }

View file

@ -20,20 +20,22 @@ const emptyTl = (userId = 0) => ({
flushMarker: 0 flushMarker: 0
}) })
export const defaultState = () => ({ const emptyNotifications = () => ({
allStatuses: [],
allStatusesObject: {},
maxId: 0,
notifications: {
desktopNotificationSilence: true, desktopNotificationSilence: true,
maxId: 0, maxId: 0,
minId: Number.POSITIVE_INFINITY, minId: Number.POSITIVE_INFINITY,
data: [], data: [],
idStore: {}, idStore: {},
loading: false, loading: false,
error: false, error: false
fetcherId: null })
},
export const defaultState = () => ({
allStatuses: [],
allStatusesObject: {},
conversationsObject: {},
maxId: 0,
notifications: emptyNotifications(),
favorites: new Set(), favorites: new Set(),
error: false, error: false,
timelines: { timelines: {
@ -111,6 +113,39 @@ const sortTimeline = (timeline) => {
return 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 }) => { const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId }) => {
// Sanity check // Sanity check
if (!isArray(statuses)) { if (!isArray(statuses)) {
@ -118,7 +153,6 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
} }
const allStatuses = state.allStatuses const allStatuses = state.allStatuses
const allStatusesObject = state.allStatusesObject
const timelineObject = state.timelines[timeline] const timelineObject = state.timelines[timeline]
const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0 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 addStatus = (data, showImmediately, addToTimeline = true) => {
const result = mergeOrAdd(allStatuses, allStatusesObject, data) const result = addStatusToGlobalStorage(state, data)
const status = result.item const status = result.item
if (result.new) { if (result.new) {
@ -235,16 +269,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
}, },
'deletion': (deletion) => { 'deletion': (deletion) => {
const uri = deletion.uri const uri = deletion.uri
// Remove possible notification
const status = find(allStatuses, {uri}) const status = find(allStatuses, {uri})
if (!status) { if (!status) {
return return
} }
remove(state.notifications.data, ({action: {id}}) => id === status.id) removeStatusFromGlobalStorage(state, status)
remove(allStatuses, { uri })
if (timeline) { if (timeline) {
remove(timelineObject.statuses, { uri }) remove(timelineObject.statuses, { uri })
remove(timelineObject.visibleStatuses, { 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 addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
const allStatuses = state.allStatuses
const allStatusesObject = state.allStatusesObject
each(notifications, (notification) => { each(notifications, (notification) => {
notification.action = mergeOrAdd(allStatuses, allStatusesObject, notification.action).item if (notification.type !== 'follow') {
notification.status = notification.status && mergeOrAdd(allStatuses, allStatusesObject, notification.status).item 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 // Only add a new notification if we don't have one for the same action
if (!state.notifications.idStore.hasOwnProperty(notification.id)) { 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') { if ('Notification' in window && window.Notification.permission === 'granted') {
const notifObj = {} const notifObj = {}
const action = notification.action const status = notification.status
const title = action.user.name const title = notification.from_profile.name
notifObj.icon = action.user.profile_image_url notifObj.icon = notification.from_profile.profile_image_url
notifObj.body = action.text // there's a problem that it doesn't put a space before links tho 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... // Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
if (action.attachments && action.attachments.length > 0 && !action.nsfw && if (status && status.attachments && status.attachments.length > 0 && !status.nsfw &&
action.attachments[0].mimetype.startsWith('image/')) { status.attachments[0].mimetype.startsWith('image/')) {
notifObj.image = action.attachments[0].url notifObj.image = status.attachments[0].url
} }
if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) { if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) {
@ -340,9 +388,6 @@ export const mutations = {
oldTimeline.visibleStatusesObject = {} oldTimeline.visibleStatusesObject = {}
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status }) each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
}, },
setNotificationFetcher (state, { fetcherId }) {
state.notifications.fetcherId = fetcherId
},
resetStatuses (state) { resetStatuses (state) {
const emptyState = defaultState() const emptyState = defaultState()
Object.entries(emptyState).forEach(([key, value]) => { Object.entries(emptyState).forEach(([key, value]) => {
@ -352,6 +397,9 @@ export const mutations = {
clearTimeline (state, { timeline }) { clearTimeline (state, { timeline }) {
state.timelines[timeline] = emptyTl(state.timelines[timeline].userId) state.timelines[timeline] = emptyTl(state.timelines[timeline].userId)
}, },
clearNotifications (state) {
state.notifications = emptyNotifications()
},
setFavorited (state, { status, value }) { setFavorited (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id] const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = value newStatus.favorited = value
@ -378,6 +426,13 @@ export const mutations = {
const newStatus = state.allStatusesObject[status.id] const newStatus = state.allStatusesObject[status.id]
newStatus.deleted = true newStatus.deleted = true
}, },
setManyDeleted (state, condition) {
Object.values(state.allStatusesObject).forEach(status => {
if (condition(status)) {
status.deleted = true
}
})
},
setLoading (state, { timeline, value }) { setLoading (state, { timeline, value }) {
state.timelines[timeline].loading = value state.timelines[timeline].loading = value
}, },
@ -413,8 +468,8 @@ const statuses = {
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) { addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) {
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId }) commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId })
}, },
addNewNotifications ({ rootState, commit, dispatch }, { notifications, older }) { addNewNotifications ({ rootState, commit, dispatch, rootGetters }, { notifications, older }) {
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older }) commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
}, },
setError ({ rootState, commit }, { value }) { setError ({ rootState, commit }, { value }) {
commit('setError', { value }) commit('setError', { value })
@ -428,16 +483,13 @@ const statuses = {
setNotificationsSilence ({ rootState, commit }, { value }) { setNotificationsSilence ({ rootState, commit }, { value }) {
commit('setNotificationsSilence', { 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) { deleteStatus ({ rootState, commit }, status) {
commit('setDeleted', { status }) commit('setDeleted', { status })
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials }) apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
}, },
markStatusesAsDeleted ({ commit }, condition) {
commit('setManyDeleted', condition)
},
favorite ({ rootState, commit }, status) { favorite ({ rootState, commit }, status) {
// Optimistic favoriting... // Optimistic favoriting...
commit('setFavorited', { status, value: true }) commit('setFavorited', { status, value: true })

View file

@ -1,5 +1,6 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' 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 { set } from 'vue'
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js' import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
import oauthApi from '../services/new_api/oauth' import oauthApi from '../services/new_api/oauth'
@ -32,11 +33,62 @@ const getNotificationPermission = () => {
return Promise.resolve(Notification.permission) 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 = { export const mutations = {
setMuted (state, { user: { id }, muted }) { setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id] const user = state.usersObject[id]
set(user, 'muted', muted) 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) { setCurrentUser (state, user) {
state.lastLoginName = user.screen_name state.lastLoginName = user.screen_name
state.currentUser = merge(state.currentUser || {}, user) state.currentUser = merge(state.currentUser || {}, user)
@ -51,42 +103,27 @@ export const mutations = {
endLogin (state) { endLogin (state) {
state.loggingIn = false state.loggingIn = false
}, },
// TODO Clean after ourselves? saveFriendIds (state, { id, friendIds }) {
addFriends (state, { id, friends }) {
const user = state.usersObject[id] const user = state.usersObject[id]
each(friends, friend => { user.friendIds = uniq(concat(user.friendIds, friendIds))
if (!find(user.friends, { id: friend.id })) {
user.friends.push(friend)
}
})
user.lastFriendId = last(friends).id
}, },
addFollowers (state, { id, followers }) { saveFollowerIds (state, { id, followerIds }) {
const user = state.usersObject[id] const user = state.usersObject[id]
each(followers, follower => { user.followerIds = uniq(concat(user.followerIds, followerIds))
if (!find(user.followers, { id: follower.id })) {
user.followers.push(follower)
}
})
user.lastFollowerId = last(followers).id
}, },
// Because frontend doesn't have a reason to keep these stuff in memory // Because frontend doesn't have a reason to keep these stuff in memory
// outside of viewing someones user profile. // outside of viewing someones user profile.
clearFriends (state, userId) { clearFriends (state, userId) {
const user = state.usersObject[userId] const user = state.usersObject[userId]
if (!user) { if (user) {
return set(user, 'friendIds', [])
} }
user.friends = []
user.lastFriendId = null
}, },
clearFollowers (state, userId) { clearFollowers (state, userId) {
const user = state.usersObject[userId] const user = state.usersObject[userId]
if (!user) { if (user) {
return set(user, 'followerIds', [])
} }
user.followers = []
user.lastFollowerId = null
}, },
addNewUsers (state, users) { addNewUsers (state, users) {
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user)) each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
@ -110,6 +147,11 @@ export const mutations = {
saveBlockIds (state, blockIds) { saveBlockIds (state, blockIds) {
state.currentUser.blockIds = blockIds state.currentUser.blockIds = blockIds
}, },
addBlockId (state, blockId) {
if (state.currentUser.blockIds.indexOf(blockId) === -1) {
state.currentUser.blockIds.push(blockId)
}
},
updateMutes (state, mutedUsers) { updateMutes (state, mutedUsers) {
// Reset muted of all fetched users // Reset muted of all fetched users
each(state.users, (user) => { user.muted = false }) each(state.users, (user) => { user.muted = false })
@ -118,12 +160,19 @@ export const mutations = {
saveMuteIds (state, muteIds) { saveMuteIds (state, muteIds) {
state.currentUser.muteIds = muteIds state.currentUser.muteIds = muteIds
}, },
addMuteId (state, muteId) {
if (state.currentUser.muteIds.indexOf(muteId) === -1) {
state.currentUser.muteIds.push(muteId)
}
},
setUserForStatus (state, status) { setUserForStatus (state, status) {
status.user = state.usersObject[status.user.id] status.user = state.usersObject[status.user.id]
}, },
setUserForNotification (state, notification) { setUserForNotification (state, notification) {
if (notification.type !== 'follow') {
notification.action.user = state.usersObject[notification.action.user.id] 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 }) { setColor (state, { user: { id }, highlighted }) {
const user = state.usersObject[id] const user = state.usersObject[id]
@ -176,8 +225,10 @@ const users = {
}) })
}, },
fetchUserRelationship (store, id) { 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)) .then((relationships) => store.commit('updateUserRelationship', relationships))
}
}, },
fetchBlocks (store) { fetchBlocks (store) {
return store.rootState.api.backendInteractor.fetchBlocks() return store.rootState.api.backendInteractor.fetchBlocks()
@ -187,18 +238,17 @@ const users = {
return blocks return blocks
}) })
}, },
blockUser (store, userId) { blockUser (store, id) {
return store.rootState.api.backendInteractor.blockUser(userId) return blockUser(store, id)
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('removeStatus', { timeline: 'friends', userId })
store.commit('removeStatus', { timeline: 'public', userId })
store.commit('removeStatus', { timeline: 'publicAndExternal', userId })
})
}, },
unblockUser (store, id) { unblockUser (store, id) {
return store.rootState.api.backendInteractor.unblockUser(id) return unblockUser(store, id)
.then((relationship) => store.commit('updateUserRelationship', [relationship])) },
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) { fetchMutes (store) {
return store.rootState.api.backendInteractor.fetchMutes() return store.rootState.api.backendInteractor.fetchMutes()
@ -209,32 +259,34 @@ const users = {
}) })
}, },
muteUser (store, id) { muteUser (store, id) {
return store.rootState.api.backendInteractor.muteUser(id) return muteUser(store, id)
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}, },
unmuteUser (store, id) { unmuteUser (store, id) {
return store.rootState.api.backendInteractor.unmuteUser(id) return unmuteUser(store, id)
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}, },
addFriends ({ rootState, commit }, fetchBy) { muteUsers (store, ids = []) {
return new Promise((resolve, reject) => { return Promise.all(ids.map(id => muteUser(store, id)))
const user = rootState.users.usersObject[fetchBy] },
const maxId = user.lastFriendId unmuteUsers (store, ids = []) {
rootState.api.backendInteractor.fetchFriends({ id: user.id, maxId }) 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) => { .then((friends) => {
commit('addFriends', { id: user.id, friends }) commit('addNewUsers', friends)
resolve(friends) commit('saveFriendIds', { id, friendIds: map(friends, 'id') })
}).catch(() => { return friends
reject()
})
}) })
}, },
addFollowers ({ rootState, commit }, fetchBy) { fetchFollowers ({ rootState, commit }, id) {
const user = rootState.users.usersObject[fetchBy] const user = rootState.users.usersObject[id]
const maxId = user.lastFollowerId const maxId = last(user.followerIds)
return rootState.api.backendInteractor.fetchFollowers({ id: user.id, maxId }) return rootState.api.backendInteractor.fetchFollowers({ id, maxId })
.then((followers) => { .then((followers) => {
commit('addFollowers', { id: user.id, followers }) commit('addNewUsers', followers)
commit('saveFollowerIds', { id, followerIds: map(followers, 'id') })
return followers return followers
}) })
}, },
@ -257,6 +309,9 @@ const users = {
unregisterPushNotifications(token) unregisterPushNotifications(token)
}, },
addNewUsers ({ commit }, users) {
commit('addNewUsers', users)
},
addNewStatuses (store, { statuses }) { addNewStatuses (store, { statuses }) {
const users = map(statuses, 'user') const users = map(statuses, 'user')
const retweetedUsers = compact(map(statuses, 'retweeted_status.user')) const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
@ -287,6 +342,14 @@ const users = {
store.commit('setUserForNotification', notification) 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) { async signUp (store, userInfo) {
store.commit('signUpPending') store.commit('signUpPending')
@ -331,7 +394,8 @@ const users = {
store.commit('setToken', false) store.commit('setToken', false)
store.dispatch('stopFetching', 'friends') store.dispatch('stopFetching', 'friends')
store.commit('setBackendInteractor', backendInteractorService()) store.commit('setBackendInteractor', backendInteractorService())
store.dispatch('stopFetchingNotifications') store.dispatch('stopFetching', 'notifications')
store.commit('clearNotifications')
store.commit('resetStatuses') store.commit('resetStatuses')
}, },
loginUser (store, accessToken) { loginUser (store, accessToken) {
@ -363,7 +427,10 @@ const users = {
} }
// Start getting fresh posts. // Start getting fresh posts.
store.dispatch('startFetching', { timeline: 'friends' }) store.dispatch('startFetchingTimeline', { timeline: 'friends' })
// Start fetching notifications
store.dispatch('startFetchingNotifications')
// Get user mutes // Get user mutes
store.dispatch('fetchMutes') store.dispatch('fetchMutes')

View file

@ -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 BANNER_UPDATE_URL = '/api/account/update_profile_banner.json'
const PROFILE_UPDATE_URL = '/api/account/update_profile.json' const PROFILE_UPDATE_URL = '/api/account/update_profile.json'
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.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 QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' 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 FOLLOW_REQUESTS_URL = '/api/pleroma/friend_requests'
const APPROVE_USER_URL = '/api/pleroma/friendships/approve' const APPROVE_USER_URL = '/api/pleroma/friendships/approve'
const DENY_USER_URL = '/api/pleroma/friendships/deny' 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 SUGGESTIONS_URL = '/api/v1/suggestions'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' 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_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite` const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog` 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_POST_STATUS_URL = '/api/v1/statuses'
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' 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 { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
import 'whatwg-fetch' import 'whatwg-fetch'
import { StatusCodeError } from '../errors/errors' import { StatusCodeError } from '../errors/errors'
@ -290,10 +294,23 @@ const fetchFriends = ({id, maxId, sinceId, limit = 20, credentials}) => {
} }
const exportFriends = ({id, credentials}) => { const exportFriends = ({id, credentials}) => {
let url = MASTODON_FOLLOWING_URL(id) + `?all=true` return new Promise(async (resolve, reject) => {
return fetch(url, { headers: authHeaders(credentials) }) try {
.then((data) => data.json()) let friends = []
.then((data) => data.map(parseUser)) 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}) => { const fetchFollowers = ({id, maxId, sinceId, limit = 20, credentials}) => {
@ -352,13 +369,93 @@ const fetchStatus = ({id, credentials}) => {
.then((data) => parseStatus(data)) .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 fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => {
const timelineUrls = { const timelineUrls = {
public: MASTODON_PUBLIC_TIMELINE, public: MASTODON_PUBLIC_TIMELINE,
friends: MASTODON_USER_HOME_TIMELINE_URL, friends: MASTODON_USER_HOME_TIMELINE_URL,
mentions: MENTIONS_URL, mentions: MENTIONS_URL,
dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL, dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
notifications: QVITTER_USER_NOTIFICATIONS_URL, notifications: MASTODON_USER_NOTIFICATIONS_URL,
'publicAndExternal': MASTODON_PUBLIC_TIMELINE, 'publicAndExternal': MASTODON_PUBLIC_TIMELINE,
user: MASTODON_USER_TIMELINE_URL, user: MASTODON_USER_TIMELINE_URL,
media: MASTODON_USER_TIMELINE_URL, media: MASTODON_USER_TIMELINE_URL,
@ -666,6 +763,12 @@ const apiService = {
fetchBlocks, fetchBlocks,
fetchOAuthTokens, fetchOAuthTokens,
revokeOAuthToken, revokeOAuthToken,
tagUser,
untagUser,
deleteUser,
addRight,
deleteRight,
setActivationStatus,
register, register,
getCaptcha, getCaptcha,
updateAvatar, updateAvatar,

View file

@ -1,5 +1,6 @@
import apiService from '../api/api.service.js' import apiService from '../api/api.service.js'
import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js' import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js'
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
const backendInteractorService = (credentials) => { const backendInteractorService = (credentials) => {
const fetchStatus = ({id}) => { const fetchStatus = ({id}) => {
@ -58,8 +59,36 @@ const backendInteractorService = (credentials) => {
return apiService.denyUser({credentials, id}) return apiService.denyUser({credentials, id})
} }
const startFetching = ({timeline, store, userId = false, tag}) => { const startFetchingTimeline = ({ timeline, store, userId = false, tag }) => {
return timelineFetcherService.startFetching({timeline, store, credentials, userId, 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}) const fetchMutes = () => apiService.fetchMutes({credentials})
@ -97,13 +126,20 @@ const backendInteractorService = (credentials) => {
fetchUserRelationship, fetchUserRelationship,
fetchAllFollowing, fetchAllFollowing,
verifyCredentials: apiService.verifyCredentials, verifyCredentials: apiService.verifyCredentials,
startFetching, startFetchingTimeline,
startFetchingNotifications,
fetchMutes, fetchMutes,
muteUser, muteUser,
unmuteUser, unmuteUser,
fetchBlocks, fetchBlocks,
fetchOAuthTokens, fetchOAuthTokens,
revokeOAuthToken, revokeOAuthToken,
tagUser,
untagUser,
addRight,
deleteRight,
deleteUser,
setActivationStatus,
register, register,
getCaptcha, getCaptcha,
updateAvatar, updateAvatar,

View file

@ -39,7 +39,7 @@ export const parseUser = (data) => {
return output return output
} }
// output.name = ??? missing output.name = data.display_name
output.name_html = addEmojis(data.display_name, data.emojis) output.name_html = addEmojis(data.display_name, data.emojis)
// output.description = ??? missing // output.description = ??? missing
@ -67,9 +67,14 @@ export const parseUser = (data) => {
output.statusnet_blocking = relationship.blocking output.statusnet_blocking = relationship.blocking
output.muted = relationship.muting 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('@') output.is_local = !output.screen_name.includes('@')
} else { } else {
output.screen_name = data.screen_name output.screen_name = data.screen_name
@ -103,7 +108,12 @@ export const parseUser = (data) => {
// QVITTER ONLY FOR NOW // QVITTER ONLY FOR NOW
// Really only applies to logged in user, really.. I THINK // 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.no_rich_text = data.no_rich_text
output.default_scope = data.default_scope output.default_scope = data.default_scope
output.hide_follows = data.hide_follows output.hide_follows = data.hide_follows
@ -119,12 +129,19 @@ export const parseUser = (data) => {
output.locked = data.locked output.locked = data.locked
output.followers_count = data.followers_count output.followers_count = data.followers_count
output.statuses_count = data.statuses_count output.statuses_count = data.statuses_count
output.friends = [] output.friendIds = []
output.followers = [] output.followerIds = []
if (data.pleroma) { if (data.pleroma) {
output.follow_request_count = data.pleroma.follow_request_count 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 return output
} }
@ -172,28 +189,28 @@ export const parseStatus = (data) => {
output.statusnet_html = addEmojis(data.content, data.emojis) 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.text = data.content
output.summary = data.spoiler_text
}
output.in_reply_to_status_id = data.in_reply_to_id output.in_reply_to_status_id = data.in_reply_to_id
output.in_reply_to_user_id = data.in_reply_to_account_id output.in_reply_to_user_id = data.in_reply_to_account_id
output.replies_count = data.replies_count 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') { if (output.type === 'retweet') {
output.retweeted_status = parseStatus(data.reblog) output.retweeted_status = parseStatus(data.reblog)
} }
output.summary = data.spoiler_text
output.summary_html = addEmojis(data.spoiler_text, data.emojis) output.summary_html = addEmojis(data.spoiler_text, data.emojis)
output.external_url = data.url output.external_url = data.url
// output.is_local = ??? missing
} else { } else {
output.favorited = data.favorited output.favorited = data.favorited
output.fave_num = data.fave_num 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_status_id = data.in_reply_to_status_id
output.in_reply_to_user_id = data.in_reply_to_user_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.in_reply_to_screen_name = data.in_reply_to_screen_name
output.statusnet_conversation_id = data.statusnet_conversation_id output.statusnet_conversation_id = data.statusnet_conversation_id
if (output.type === 'retweet') { if (output.type === 'retweet') {
@ -272,9 +288,11 @@ export const parseNotification = (data) => {
if (masto) { if (masto) {
output.type = mastoDict[data.type] || data.type output.type = mastoDict[data.type] || data.type
// output.seen = ??? missing output.seen = data.pleroma.is_seen
output.status = parseStatus(data.status) output.status = output.type === 'follow'
output.action = output.status // not sure ? null
: parseStatus(data.status)
output.action = output.status // TODO: Refactor, this is unneeded
output.from_profile = parseUser(data.account) output.from_profile = parseUser(data.account)
} else { } else {
const parsedNotice = parseStatus(data.notice) const parsedNotice = parseStatus(data.notice)

View file

@ -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 // For locked users we just mark it that we sent the follow request
if (updated.locked) { if (updated.locked) {
resolve({ resolve({ sent: true })
sent: true,
updated
})
} }
if (updated.following) { if (updated.following) {
// If we get result immediately, just stop. // If we get result immediately, just stop.
resolve({ resolve({ sent: false })
sent: false,
updated
})
} }
// But usually we don't get result immediately, so we ask server // 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) => { .then((following) => {
if (following) { if (following) {
// We confirmed and everything's good. // We confirmed and everything's good.
resolve({ resolve({ sent: false })
sent: false,
updated
})
} else { } else {
// If after all the tries, just treat it as if user is locked // If after all the tries, just treat it as if user is locked
resolve({ resolve({ sent: false })
sent: false,
updated
})
} }
}) })
}) })

View file

@ -10,8 +10,8 @@ export const visibleTypes = store => ([
].filter(_ => _)) ].filter(_ => _))
const sortById = (a, b) => { const sortById = (a, b) => {
const seqA = Number(a.action.id) const seqA = Number(a.id)
const seqB = Number(b.action.id) const seqB = Number(b.id)
const isSeqA = !Number.isNaN(seqA) const isSeqA = !Number.isNaN(seqA)
const isSeqB = !Number.isNaN(seqB) const isSeqB = !Number.isNaN(seqB)
if (isSeqA && isSeqB) { if (isSeqA && isSeqB) {
@ -21,7 +21,7 @@ const sortById = (a, b) => {
} else if (!isSeqA && isSeqB) { } else if (!isSeqA && isSeqB) {
return -1 return -1
} else { } else {
return a.action.id > b.action.id ? -1 : 1 return a.id > b.id ? -1 : 1
} }
} }

View file

@ -11,29 +11,35 @@ const fetchAndUpdate = ({store, credentials, older = false}) => {
const rootState = store.rootState || store.state const rootState = store.rootState || store.state
const timelineData = rootState.statuses.notifications const timelineData = rootState.statuses.notifications
args['timeline'] = 'notifications'
if (older) { if (older) {
if (timelineData.minId !== Number.POSITIVE_INFINITY) { if (timelineData.minId !== Number.POSITIVE_INFINITY) {
args['until'] = timelineData.minId args['until'] = timelineData.minId
} }
return fetchNotifications({ store, args, older })
} else { } 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 notifications = timelineData.data
const unread = notifications.filter(n => !n.seen).map(n => n.id) const unread = notifications.filter(n => !n.seen).map(n => n.id)
if (!unread.length) { if (unread.length) {
args['since'] = timelineData.maxId args['since'] = Math.min(...unread)
} else { fetchNotifications({ store, args, older })
args['since'] = Math.min(...unread) - 1
if (timelineData.maxId !== Math.max(...unread)) {
args['until'] = Math.max(...unread, args['since'] + 20)
}
}
} }
args['timeline'] = 'notifications' return result
}
}
const fetchNotifications = ({ store, args, older }) => {
return apiService.fetchTimeline(args) return apiService.fetchTimeline(args)
.then((notifications) => { .then((notifications) => {
update({store, notifications, older}) update({ store, notifications, older })
return notifications return notifications
}, () => store.dispatch('setNotificationsError', { value: true })) }, () => store.dispatch('setNotificationsError', { value: true }))
.catch(() => store.dispatch('setNotificationsError', { value: true })) .catch(() => store.dispatch('setNotificationsError', { value: true }))

View file

@ -245,6 +245,12 @@
"css": "bell-alt", "css": "bell-alt",
"code": 61683, "code": 61683,
"src": "fontawesome" "src": "fontawesome"
},
{
"uid": "5bb103cd29de77e0e06a52638527b575",
"css": "wrench",
"code": 59418,
"src": "fontawesome"
} }
] ]
} }

View file

@ -24,6 +24,8 @@
.icon-adjust:before { content: '\e816'; } /* '' */ .icon-adjust:before { content: '\e816'; } /* '' */
.icon-edit:before { content: '\e817'; } /* '' */ .icon-edit:before { content: '\e817'; } /* '' */
.icon-pencil:before { content: '\e818'; } /* '' */ .icon-pencil:before { content: '\e818'; } /* '' */
.icon-verified:before { content: '\e819'; } /* '' */
.icon-wrench:before { content: '\e81a'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */ .icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */ .icon-spin4:before { content: '\e834'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */ .icon-link-ext:before { content: '\f08e'; } /* '' */

File diff suppressed because one or more lines are too long

View file

@ -24,6 +24,8 @@
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); } .icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); }
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe817;&nbsp;'); } .icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe817;&nbsp;'); }
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); } .icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); }
.icon-verified { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe819;&nbsp;'); }
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81a;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); } .icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); } .icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); } .icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }

View file

@ -35,6 +35,8 @@
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); } .icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); }
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe817;&nbsp;'); } .icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe817;&nbsp;'); }
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); } .icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); }
.icon-verified { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe819;&nbsp;'); }
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81a;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); } .icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); } .icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); } .icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }

View file

@ -1,11 +1,11 @@
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('../font/fontello.eot?72648396'); src: url('../font/fontello.eot?11878820');
src: url('../font/fontello.eot?72648396#iefix') format('embedded-opentype'), src: url('../font/fontello.eot?11878820#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?72648396') format('woff2'), url('../font/fontello.woff2?11878820') format('woff2'),
url('../font/fontello.woff?72648396') format('woff'), url('../font/fontello.woff?11878820') format('woff'),
url('../font/fontello.ttf?72648396') format('truetype'), url('../font/fontello.ttf?11878820') format('truetype'),
url('../font/fontello.svg?72648396#fontello') format('svg'); url('../font/fontello.svg?11878820#fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face { @font-face {
font-family: 'fontello'; 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-adjust:before { content: '\e816'; } /* '' */
.icon-edit:before { content: '\e817'; } /* '' */ .icon-edit:before { content: '\e817'; } /* '' */
.icon-pencil:before { content: '\e818'; } /* '' */ .icon-pencil:before { content: '\e818'; } /* '' */
.icon-verified:before { content: '\e819'; } /* '' */
.icon-wrench:before { content: '\e81a'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */ .icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */ .icon-spin4:before { content: '\e834'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */ .icon-link-ext:before { content: '\f08e'; } /* '' */

26
static/font/demo.html Normal file → Executable file
View file

@ -229,11 +229,11 @@ body {
} }
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('./font/fontello.eot?23081587'); src: url('./font/fontello.eot?60799712');
src: url('./font/fontello.eot?23081587#iefix') format('embedded-opentype'), src: url('./font/fontello.eot?60799712#iefix') format('embedded-opentype'),
url('./font/fontello.woff?23081587') format('woff'), url('./font/fontello.woff?60799712') format('woff'),
url('./font/fontello.ttf?23081587') format('truetype'), url('./font/fontello.ttf?60799712') format('truetype'),
url('./font/fontello.svg?23081587#fontello') format('svg'); url('./font/fontello.svg?60799712#fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -335,25 +335,29 @@ body {
</div> </div>
<div class="row"> <div class="row">
<div class="the-icons span3" title="Code: 0xe818"><i class="demo-icon icon-pencil">&#xe818;</i> <span class="i-name">icon-pencil</span><span class="i-code">0xe818</span></div> <div class="the-icons span3" title="Code: 0xe818"><i class="demo-icon icon-pencil">&#xe818;</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">&#xe819;</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">&#xe81a;</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">&#xe832;</i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div> <div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin">&#xe832;</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">&#xe834;</i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div> <div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin">&#xe834;</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">&#xf08e;</i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div> <div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext">&#xf08e;</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">&#xf08f;</i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div> <div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt">&#xf08f;</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">&#xf0c9;</i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div> <div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu">&#xf0c9;</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">&#xf0e0;</i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div> <div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt">&#xf0e0;</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">&#xf0e5;</i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div> <div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty">&#xf0e5;</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">&#xf0f3;</i> <span class="i-name">icon-bell-alt</span><span class="i-code">0xf0f3</span></div> <div class="the-icons span3" title="Code: 0xf0f3"><i class="demo-icon icon-bell-alt">&#xf0f3;</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">&#xf0fe;</i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div> <div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared">&#xf0fe;</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">&#xf112;</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">&#xf13e;</i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div>
</div> </div>
<div class="row"> <div class="row">
<div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply">&#xf112;</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">&#xf13e;</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">&#xf144;</i> <span class="i-name">icon-play-circled</span><span class="i-code">0xf144</span></div> <div class="the-icons span3" title="Code: 0xf144"><i class="demo-icon icon-play-circled">&#xf144;</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">&#xf164;</i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div> <div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt">&#xf164;</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">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div> <div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars">&#xf1e5;</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">&#xf234;</i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div> <div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus">&#xf234;</i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div>
</div> </div>

Binary file not shown.

View file

@ -56,6 +56,10 @@
<glyph glyph-name="pencil" unicode="&#xe818;" 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="pencil" unicode="&#xe818;" 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="&#xe819;" 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="&#xe81a;" 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="&#xe832;" 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="spin3" unicode="&#xe832;" 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="&#xe834;" 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" /> <glyph glyph-name="spin4" unicode="&#xe834;" 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.

View file

@ -58,7 +58,10 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639", "uri": "https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -127,7 +130,10 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/6634d32b-96a8-4852-a3db-ac8730715779", "uri": "https://shigusegubu.club/objects/6634d32b-96a8-4852-a3db-ac8730715779",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -250,7 +256,10 @@
"tags": [], "tags": [],
"uri": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b", "uri": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b",
"url": "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, "reblogged": false,
"reblogs_count": 0, "reblogs_count": 0,
@ -260,7 +269,10 @@
"tags": [], "tags": [],
"uri": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b", "uri": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -329,7 +341,10 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/0f963ca1-a263-41ca-a43c-b5d26d0a08e9", "uri": "https://shigusegubu.club/objects/0f963ca1-a263-41ca-a43c-b5d26d0a08e9",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -390,7 +405,10 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/3f809bd8-656f-4a29-81d4-80eed6916eb0", "uri": "https://shigusegubu.club/objects/3f809bd8-656f-4a29-81d4-80eed6916eb0",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -516,7 +534,10 @@
}], }],
"uri": "tag:social.super-niche.club,2019-01-17:noticeId=2353002:objectType=note", "uri": "tag:social.super-niche.club,2019-01-17:noticeId=2353002:objectType=note",
"url": "https://social.super-niche.club/notice/2353002", "url": "https://social.super-niche.club/notice/2353002",
"visibility": "public" "visibility": "public",
"pleroma": {
"local": true
}
}, },
"reblogged": false, "reblogged": false,
"reblogs_count": 0, "reblogs_count": 0,
@ -529,7 +550,10 @@
}], }],
"uri": "tag:social.super-niche.club,2019-01-17:noticeId=2353002:objectType=note", "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", "url": "tag:social.super-niche.club,2019-01-17:noticeId=2353002:objectType=note",
"visibility": "public" "visibility": "public",
"pleroma": {
"local": true
}
}, { }, {
"account": { "account": {
"acct": "hj", "acct": "hj",
@ -657,7 +681,10 @@
"tags": [], "tags": [],
"uri": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab", "uri": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab",
"url": "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, "reblogged": false,
"reblogs_count": 0, "reblogs_count": 0,
@ -667,7 +694,10 @@
"tags": [], "tags": [],
"uri": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab", "uri": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -733,7 +763,10 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/38b1bc44-15d8-40dd-b1aa-937e0ff4a86d", "uri": "https://shigusegubu.club/objects/38b1bc44-15d8-40dd-b1aa-937e0ff4a86d",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -794,7 +827,10 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/fbff5da4-a517-42a9-bca9-17cae8cf2542", "uri": "https://shigusegubu.club/objects/fbff5da4-a517-42a9-bca9-17cae8cf2542",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -850,7 +886,10 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/4007d659-27c6-4577-be10-fd134f5e4e7e", "uri": "https://shigusegubu.club/objects/4007d659-27c6-4577-be10-fd134f5e4e7e",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -906,7 +945,10 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/59912d51-1cc6-4dc7-828c-f167e6c8b391", "uri": "https://shigusegubu.club/objects/59912d51-1cc6-4dc7-828c-f167e6c8b391",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -962,7 +1004,10 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/62690bce-3f49-4047-9c8e-8941f2f79e10", "uri": "https://shigusegubu.club/objects/62690bce-3f49-4047-9c8e-8941f2f79e10",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -1023,7 +1068,10 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/818f3dd0-2ff8-4def-a170-e4d4c405f387", "uri": "https://shigusegubu.club/objects/818f3dd0-2ff8-4def-a170-e4d4c405f387",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -1084,7 +1132,10 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/0783a193-c097-488d-8944-47df9372cd6e", "uri": "https://shigusegubu.club/objects/0783a193-c097-488d-8944-47df9372cd6e",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -1145,7 +1196,10 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/145d5252-7b8e-467d-9f36-1db0818f452f", "uri": "https://shigusegubu.club/objects/145d5252-7b8e-467d-9f36-1db0818f452f",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -1252,7 +1306,10 @@
"tags": [], "tags": [],
"uri": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a", "uri": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a",
"url": "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, "reblogged": false,
"reblogs_count": 0, "reblogs_count": 0,
@ -1262,7 +1319,10 @@
"tags": [], "tags": [],
"uri": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a", "uri": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -1323,7 +1383,10 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/d4eb7c46-02f9-4b1f-83af-926cefa21f33", "uri": "https://shigusegubu.club/objects/d4eb7c46-02f9-4b1f-83af-926cefa21f33",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -1446,7 +1509,10 @@
"tags": [], "tags": [],
"uri": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733", "uri": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733",
"url": "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, "reblogged": false,
"reblogs_count": 0, "reblogs_count": 0,
@ -1456,7 +1522,10 @@
"tags": [], "tags": [],
"uri": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733", "uri": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -1517,7 +1586,10 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/f472f4ed-8b0b-492f-9d53-d69eda79629d", "uri": "https://shigusegubu.club/objects/f472f4ed-8b0b-492f-9d53-d69eda79629d",
"url": "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": { "account": {
"acct": "hj", "acct": "hj",
@ -1578,5 +1650,8 @@
"tags": [], "tags": [],
"uri": "https://shigusegubu.club/objects/d6fb4fd2-1f6a-4446-a1a6-5edd34050096", "uri": "https://shigusegubu.club/objects/d6fb4fd2-1f6a-4446-a1a6-5edd34050096",
"url": "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
}
}] }]

View file

@ -129,7 +129,10 @@ const makeMockStatusMasto = (overrides = {}) => {
tags: [], tags: [],
uri: 'https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639', uri: 'https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639',
url: '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) }, overrides)
} }

View file

@ -9,14 +9,17 @@ describe('NotificationUtils', () => {
notifications: { notifications: {
data: [ data: [
{ {
id: 1,
action: { id: '1' }, action: { id: '1' },
type: 'like' type: 'like'
}, },
{ {
id: 2,
action: { id: '2' }, action: { id: '2' },
type: 'mention' type: 'mention'
}, },
{ {
id: 3,
action: { id: '3' }, action: { id: '3' },
type: 'repeat' type: 'repeat'
} }
@ -35,10 +38,12 @@ describe('NotificationUtils', () => {
const expected = [ const expected = [
{ {
action: { id: '3' }, action: { id: '3' },
id: 3,
type: 'repeat' type: 'repeat'
}, },
{ {
action: { id: '1' }, action: { id: '1' },
id: 1,
type: 'like' type: 'like'
} }
] ]

View file

@ -6097,6 +6097,10 @@ pkg-dir@^3.0.0:
dependencies: dependencies:
find-up "^3.0.0" 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: posix-character-classes@^0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" 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" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== 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: validate-npm-package-license@^3.0.1:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" 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" 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== 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: vue-i18n@^7.3.2:
version "7.8.1" version "7.8.1"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-7.8.1.tgz#2ce4b6efde679a1e05ddb5d907bfc1bc218803b2" 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-style-loader "^4.0.1"
vue-template-es2015-compiler "^1.6.0" 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: vue-router@^3.0.1:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be" resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be"