Merge remote-tracking branch 'upstream/develop' into shigusegubu
* upstream/develop: (57 commits) Feature/add sticker picker guard more secure routes guard secure routes by redirecting to root closest can returns itself as well find inside status-content div only try to use the closest a tag as target Update es.json Also apply keyword filter to subjects Remove files I accidentally pushed in fix issues caused by merges in usersearch on @ Add user search at fix eslint warnings remove vue-popperjs fix moderation menu partially hidden by usercard boundary migrate popper css rewrite ModerationTools using v-tooltip make popover position for status action dropdow relative to parent node rewrite ExtraButtons using v-tooltip install v-tooltip i18n/Update pedantic Japanese translation ...
This commit is contained in:
commit
bbcd3190f2
62 changed files with 1347 additions and 557 deletions
|
@ -25,14 +25,13 @@
|
|||
"localforage": "^1.5.0",
|
||||
"object-path": "^0.11.3",
|
||||
"phoenix": "^1.3.0",
|
||||
"popper.js": "^1.14.7",
|
||||
"portal-vue": "^2.1.4",
|
||||
"sanitize-html": "^1.13.0",
|
||||
"v-click-outside": "^2.1.1",
|
||||
"v-tooltip": "^2.0.2",
|
||||
"vue": "^2.5.13",
|
||||
"vue-chat-scroll": "^1.2.1",
|
||||
"vue-i18n": "^7.3.2",
|
||||
"vue-popperjs": "^2.0.3",
|
||||
"vue-router": "^3.0.1",
|
||||
"vue-template-compiler": "^2.3.4",
|
||||
"vuelidate": "^0.7.4",
|
||||
|
@ -81,8 +80,8 @@
|
|||
"json-loader": "^0.5.4",
|
||||
"karma": "^3.0.0",
|
||||
"karma-coverage": "^1.1.1",
|
||||
"karma-mocha": "^1.2.0",
|
||||
"karma-firefox-launcher": "^1.1.0",
|
||||
"karma-mocha": "^1.2.0",
|
||||
"karma-sinon-chai": "^2.0.2",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-spec-reporter": "0.0.26",
|
||||
|
|
12
src/App.js
12
src/App.js
|
@ -1,7 +1,7 @@
|
|||
import UserPanel from './components/user_panel/user_panel.vue'
|
||||
import NavPanel from './components/nav_panel/nav_panel.vue'
|
||||
import Notifications from './components/notifications/notifications.vue'
|
||||
import UserFinder from './components/user_finder/user_finder.vue'
|
||||
import SearchBar from './components/search_bar/search_bar.vue'
|
||||
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
|
||||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||
|
@ -19,7 +19,7 @@ export default {
|
|||
UserPanel,
|
||||
NavPanel,
|
||||
Notifications,
|
||||
UserFinder,
|
||||
SearchBar,
|
||||
InstanceSpecificPanel,
|
||||
FeaturesPanel,
|
||||
WhoToFollowPanel,
|
||||
|
@ -32,7 +32,7 @@ export default {
|
|||
},
|
||||
data: () => ({
|
||||
mobileActivePanel: 'timeline',
|
||||
finderHidden: true,
|
||||
searchBarHidden: true,
|
||||
supportsMask: window.CSS && window.CSS.supports && (
|
||||
window.CSS.supports('mask-size', 'contain') ||
|
||||
window.CSS.supports('-webkit-mask-size', 'contain') ||
|
||||
|
@ -70,7 +70,7 @@ export default {
|
|||
logoBgStyle () {
|
||||
return Object.assign({
|
||||
'margin': `${this.$store.state.instance.logoMargin} 0`,
|
||||
opacity: this.finderHidden ? 1 : 0
|
||||
opacity: this.searchBarHidden ? 1 : 0
|
||||
}, this.enableMask ? {} : {
|
||||
'background-color': this.enableMask ? '' : 'transparent'
|
||||
})
|
||||
|
@ -101,8 +101,8 @@ export default {
|
|||
this.$router.replace('/main/public')
|
||||
this.$store.dispatch('logout')
|
||||
},
|
||||
onFinderToggled (hidden) {
|
||||
this.finderHidden = hidden
|
||||
onSearchBarToggled (hidden) {
|
||||
this.searchBarHidden = hidden
|
||||
},
|
||||
updateMobileState () {
|
||||
const mobileLayout = windowWidth() <= 800
|
||||
|
|
25
src/App.scss
25
src/App.scss
|
@ -283,6 +283,31 @@ i[class*=icon-] {
|
|||
color: var(--icon, $fallback--icon)
|
||||
}
|
||||
|
||||
.btn-block {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
|
||||
button {
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
|
|
|
@ -38,9 +38,9 @@
|
|||
</router-link>
|
||||
</div>
|
||||
<div class="item right">
|
||||
<user-finder
|
||||
class="button-icon nav-icon mobile-hidden"
|
||||
@toggled="onFinderToggled"
|
||||
<search-bar
|
||||
class="nav-icon mobile-hidden"
|
||||
@toggled="onSearchBarToggled"
|
||||
/>
|
||||
<router-link
|
||||
class="mobile-hidden"
|
||||
|
|
|
@ -148,6 +148,37 @@ const getInstancePanel = async ({ store }) => {
|
|||
}
|
||||
}
|
||||
|
||||
const getStickers = async ({ store }) => {
|
||||
try {
|
||||
const res = await window.fetch('/static/stickers.json')
|
||||
if (res.ok) {
|
||||
const values = await res.json()
|
||||
const stickers = (await Promise.all(
|
||||
Object.entries(values).map(async ([name, path]) => {
|
||||
const resPack = await window.fetch(path + 'pack.json')
|
||||
var meta = {}
|
||||
if (resPack.ok) {
|
||||
meta = await resPack.json()
|
||||
}
|
||||
return {
|
||||
pack: name,
|
||||
path,
|
||||
meta
|
||||
}
|
||||
})
|
||||
)).sort((a, b) => {
|
||||
return a.meta.title.localeCompare(b.meta.title)
|
||||
})
|
||||
store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
|
||||
} else {
|
||||
throw (res)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Can't load stickers")
|
||||
console.warn(e)
|
||||
}
|
||||
}
|
||||
|
||||
const getStaticEmoji = async ({ store }) => {
|
||||
try {
|
||||
const res = await window.fetch('/static/emoji.json')
|
||||
|
@ -286,6 +317,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||
setConfig({ store }),
|
||||
getTOS({ store }),
|
||||
getInstancePanel({ store }),
|
||||
getStickers({ store }),
|
||||
getStaticEmoji({ store }),
|
||||
getCustomEmoji({ store }),
|
||||
getNodeInfo({ store })
|
||||
|
|
|
@ -6,12 +6,12 @@ import ConversationPage from 'components/conversation-page/conversation-page.vue
|
|||
import Interactions from 'components/interactions/interactions.vue'
|
||||
import DMs from 'components/dm_timeline/dm_timeline.vue'
|
||||
import UserProfile from 'components/user_profile/user_profile.vue'
|
||||
import Search from 'components/search/search.vue'
|
||||
import Settings from 'components/settings/settings.vue'
|
||||
import Registration from 'components/registration/registration.vue'
|
||||
import UserSettings from 'components/user_settings/user_settings.vue'
|
||||
import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
||||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||
import UserSearch from 'components/user_search/user_search.vue'
|
||||
import Notifications from 'components/notifications/notifications.vue'
|
||||
import AuthForm from 'components/auth_form/auth_form.js'
|
||||
import ChatPanel from 'components/chat_panel/chat_panel.vue'
|
||||
|
@ -19,6 +19,14 @@ import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
|||
import About from 'components/about/about.vue'
|
||||
|
||||
export default (store) => {
|
||||
const validateAuthenticatedRoute = (to, from, next) => {
|
||||
if (store.state.users.currentUser) {
|
||||
next()
|
||||
} else {
|
||||
next(store.state.instance.redirectRootNoLogin || '/main/all')
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
{ name: 'root',
|
||||
path: '/',
|
||||
|
@ -30,23 +38,23 @@ export default (store) => {
|
|||
},
|
||||
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
|
||||
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
|
||||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline },
|
||||
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
|
||||
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
|
||||
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
|
||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions },
|
||||
{ name: 'dms', path: '/users/:username/dms', component: DMs },
|
||||
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'settings', path: '/settings', component: Settings },
|
||||
{ name: 'registration', path: '/registration', component: Registration },
|
||||
{ name: 'registration-token', path: '/registration/:token', component: Registration },
|
||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
|
||||
{ name: 'user-settings', path: '/user-settings', component: UserSettings },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications },
|
||||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'login', path: '/login', component: AuthForm },
|
||||
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||
{ name: 'user-search', path: '/user-search', component: UserSearch, props: (route) => ({ query: route.query.query }) },
|
||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow },
|
||||
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||
{ name: 'about', path: '/about', component: About },
|
||||
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
|
||||
]
|
||||
|
|
|
@ -100,7 +100,7 @@
|
|||
<!-- eslint-disable vue/no-v-html -->
|
||||
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
|
||||
<div v-html="attachment.oembed.oembedHTML" />
|
||||
<!-- eslint-enabled vue/no-v-html -->
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,20 +1,27 @@
|
|||
import { debounce } from 'lodash'
|
||||
/**
|
||||
* suggest - generates a suggestor function to be used by emoji-input
|
||||
* data: object providing source information for specific types of suggestions:
|
||||
* data.emoji - optional, an array of all emoji available i.e.
|
||||
* (state.instance.emoji + state.instance.customEmoji)
|
||||
* data.users - optional, an array of all known users
|
||||
* updateUsersList - optional, a function to search and append to users
|
||||
*
|
||||
* Depending on data present one or both (or none) can be present, so if field
|
||||
* doesn't support user linking you can just provide only emoji.
|
||||
*/
|
||||
|
||||
const debounceUserSearch = debounce((data, input) => {
|
||||
data.updateUsersList(input)
|
||||
}, 500, { leading: true, trailing: false })
|
||||
|
||||
export default data => input => {
|
||||
const firstChar = input[0]
|
||||
if (firstChar === ':' && data.emoji) {
|
||||
return suggestEmoji(data.emoji)(input)
|
||||
}
|
||||
if (firstChar === '@' && data.users) {
|
||||
return suggestUsers(data.users)(input)
|
||||
return suggestUsers(data)(input)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
@ -38,9 +45,11 @@ export const suggestEmoji = emojis => input => {
|
|||
})
|
||||
}
|
||||
|
||||
export const suggestUsers = users => input => {
|
||||
export const suggestUsers = data => input => {
|
||||
const noPrefix = input.toLowerCase().substr(1)
|
||||
return users.filter(
|
||||
const users = data.users
|
||||
|
||||
const newUsers = users.filter(
|
||||
user =>
|
||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||
user.name.toLowerCase().startsWith(noPrefix)
|
||||
|
@ -75,5 +84,11 @@ export const suggestUsers = users => input => {
|
|||
imageUrl: profile_image_url_original,
|
||||
replacement: '@' + screen_name + ' '
|
||||
}))
|
||||
|
||||
// BE search users if there are no matches
|
||||
if (newUsers.length === 0 && data.updateUsersList) {
|
||||
debounceUserSearch(data, noPrefix)
|
||||
}
|
||||
return newUsers
|
||||
/* eslint-enable camelcase */
|
||||
}
|
||||
|
|
|
@ -1,45 +1,21 @@
|
|||
import Popper from 'vue-popperjs/src/component/popper.js.vue'
|
||||
|
||||
const ExtraButtons = {
|
||||
props: [ 'status' ],
|
||||
components: {
|
||||
Popper
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showDropDown: false,
|
||||
showPopper: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteStatus () {
|
||||
this.refreshPopper()
|
||||
const confirmed = window.confirm(this.$t('status.delete_confirm'))
|
||||
if (confirmed) {
|
||||
this.$store.dispatch('deleteStatus', { id: this.status.id })
|
||||
}
|
||||
},
|
||||
toggleMenu () {
|
||||
this.showDropDown = !this.showDropDown
|
||||
},
|
||||
pinStatus () {
|
||||
this.refreshPopper()
|
||||
this.$store.dispatch('pinStatus', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
unpinStatus () {
|
||||
this.refreshPopper()
|
||||
this.$store.dispatch('unpinStatus', this.status.id)
|
||||
.then(() => this.$emit('onSuccess'))
|
||||
.catch(err => this.$emit('onError', err.error.error))
|
||||
},
|
||||
refreshPopper () {
|
||||
this.showPopper = false
|
||||
this.showDropDown = false
|
||||
setTimeout(() => {
|
||||
this.showPopper = true
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -1,21 +1,17 @@
|
|||
<template>
|
||||
<Popper
|
||||
v-if="enabled && showPopper"
|
||||
<v-popover
|
||||
v-if="enabled"
|
||||
trigger="click"
|
||||
append-to-body
|
||||
:options="{
|
||||
placement: 'top',
|
||||
modifiers: {
|
||||
arrow: { enabled: true },
|
||||
offset: { offset: '0, 5px' },
|
||||
}
|
||||
}"
|
||||
@hide="showDropDown = false"
|
||||
placement="top"
|
||||
class="extra-button-popover"
|
||||
:offset="5"
|
||||
:container="false"
|
||||
>
|
||||
<div class="popper-wrapper">
|
||||
<div slot="popover">
|
||||
<div class="dropdown-menu">
|
||||
<button
|
||||
v-if="!status.pinned && canPin"
|
||||
v-close-popover
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="pinStatus"
|
||||
>
|
||||
|
@ -23,6 +19,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="status.pinned && canPin"
|
||||
v-close-popover
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="unpinStatus"
|
||||
>
|
||||
|
@ -30,6 +27,7 @@
|
|||
</button>
|
||||
<button
|
||||
v-if="canDelete"
|
||||
v-close-popover
|
||||
class="dropdown-item dropdown-item-icon"
|
||||
@click.prevent="deleteStatus"
|
||||
>
|
||||
|
@ -37,17 +35,10 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
slot="reference"
|
||||
class="button-icon"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
<i
|
||||
class="icon-ellipsis"
|
||||
:class="{'icon-clicked': showDropDown}"
|
||||
/>
|
||||
<div class="button-icon">
|
||||
<i class="icon-ellipsis" />
|
||||
</div>
|
||||
</Popper>
|
||||
</v-popover>
|
||||
</template>
|
||||
|
||||
<script src="./extra_buttons.js" ></script>
|
||||
|
@ -59,7 +50,8 @@
|
|||
.icon-ellipsis {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover, &.icon-clicked {
|
||||
&:hover,
|
||||
.extra-button-popover.open & {
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
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'
|
||||
|
@ -29,8 +28,7 @@ const ModerationTools = {
|
|||
}
|
||||
},
|
||||
components: {
|
||||
DialogModal,
|
||||
Popper
|
||||
DialogModal
|
||||
},
|
||||
computed: {
|
||||
tagsSet () {
|
||||
|
@ -41,9 +39,6 @@ const ModerationTools = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
toggleMenu () {
|
||||
this.showDropDown = !this.showDropDown
|
||||
},
|
||||
hasTag (tagName) {
|
||||
return this.tagsSet.has(tagName)
|
||||
},
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
<template>
|
||||
<div
|
||||
class="block"
|
||||
style="position: relative"
|
||||
>
|
||||
<Popper
|
||||
<div>
|
||||
<v-popover
|
||||
trigger="click"
|
||||
append-to-body
|
||||
:options="{
|
||||
placement: 'bottom-end',
|
||||
modifiers: {
|
||||
arrow: { enabled: true },
|
||||
offset: { offset: '0, 5px' },
|
||||
}
|
||||
}"
|
||||
class="moderation-tools-popover"
|
||||
:container="false"
|
||||
placement="bottom-end"
|
||||
:offset="5"
|
||||
@show="showDropDown = true"
|
||||
@hide="showDropDown = false"
|
||||
>
|
||||
<div class="popper-wrapper">
|
||||
<div slot="popover">
|
||||
<div class="dropdown-menu">
|
||||
<span v-if="user.is_local">
|
||||
<button
|
||||
|
@ -130,13 +124,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<button
|
||||
slot="reference"
|
||||
class="btn btn-default btn-block"
|
||||
:class="{ pressed: showDropDown }"
|
||||
@click="toggleMenu"
|
||||
>
|
||||
{{ $t('user_card.admin_menu.moderation') }}
|
||||
</button>
|
||||
</Popper>
|
||||
</v-popover>
|
||||
<portal to="modal">
|
||||
<DialogModal
|
||||
v-if="showDeleteUserDialog"
|
||||
|
@ -190,4 +183,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.moderation-tools-popover {
|
||||
height: 100%;
|
||||
.trigger {
|
||||
display: flex !important;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,71 +1,99 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.popper-wrapper {
|
||||
.tooltip.popover {
|
||||
z-index: 8;
|
||||
}
|
||||
|
||||
.popper-wrapper .popper__arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
margin: 5px;
|
||||
}
|
||||
.popover-inner {
|
||||
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
|
||||
box-shadow: var(--panelShadow);
|
||||
border-radius: $fallback--btnRadius;
|
||||
border-radius: var(--btnRadius, $fallback--btnRadius);
|
||||
background-color: $fallback--bg;
|
||||
background-color: var(--bg, $fallback--bg);
|
||||
}
|
||||
|
||||
.popper-wrapper[x-placement^="top"] {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.popover-arrow {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
position: absolute;
|
||||
margin: 5px;
|
||||
border-color: $fallback--bg;
|
||||
border-color: var(--bg, $fallback--bg);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
&[x-placement^="top"] {
|
||||
margin-bottom: 5px;
|
||||
|
||||
.popper-wrapper[x-placement^="bottom"] {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.popover-arrow {
|
||||
border-width: 5px 5px 0 5px;
|
||||
border-left-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
bottom: -5px;
|
||||
left: calc(50% - 5px);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
&[x-placement^="bottom"] {
|
||||
margin-top: 5px;
|
||||
|
||||
.popper-wrapper[x-placement^="right"] {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.popover-arrow {
|
||||
border-width: 0 5px 5px 5px;
|
||||
border-left-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-top-color: transparent !important;
|
||||
top: -5px;
|
||||
left: calc(50% - 5px);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
&[x-placement^="right"] {
|
||||
margin-left: 5px;
|
||||
|
||||
.popper-wrapper[x-placement^="left"] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.popover-arrow {
|
||||
border-width: 5px 5px 5px 0;
|
||||
border-left-color: transparent !important;
|
||||
border-top-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
left: -5px;
|
||||
top: calc(50% - 5px);
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
&[x-placement^="left"] {
|
||||
margin-right: 5px;
|
||||
|
||||
.popover-arrow {
|
||||
border-width: 5px 0 5px 5px;
|
||||
border-top-color: transparent !important;
|
||||
border-right-color: transparent !important;
|
||||
border-bottom-color: transparent !important;
|
||||
right: -5px;
|
||||
top: calc(50% - 5px);
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&[aria-hidden='true'] {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity .15s, visibility .15s;
|
||||
}
|
||||
|
||||
&[aria-hidden='false'] {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
transition: opacity .15s;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
|
@ -76,13 +104,6 @@
|
|||
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;
|
||||
|
|
|
@ -3,6 +3,7 @@ import MediaUpload from '../media_upload/media_upload.vue'
|
|||
import ScopeSelector from '../scope_selector/scope_selector.vue'
|
||||
import EmojiInput from '../emoji-input/emoji-input.vue'
|
||||
import PollForm from '../poll/poll_form.vue'
|
||||
import StickerPicker from '../sticker_picker/sticker_picker.vue'
|
||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
import { reject, map, uniqBy } from 'lodash'
|
||||
import suggestor from '../emoji-input/suggestor.js'
|
||||
|
@ -34,6 +35,7 @@ const PostStatusForm = {
|
|||
MediaUpload,
|
||||
EmojiInput,
|
||||
PollForm,
|
||||
StickerPicker,
|
||||
ScopeSelector
|
||||
},
|
||||
mounted () {
|
||||
|
@ -82,7 +84,8 @@ const PostStatusForm = {
|
|||
contentType
|
||||
},
|
||||
caret: 0,
|
||||
pollFormVisible: false
|
||||
pollFormVisible: false,
|
||||
stickerPickerVisible: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -104,7 +107,8 @@ const PostStatusForm = {
|
|||
...this.$store.state.instance.emoji,
|
||||
...this.$store.state.instance.customEmoji
|
||||
],
|
||||
users: this.$store.state.users.users
|
||||
users: this.$store.state.users.users,
|
||||
updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
|
||||
})
|
||||
},
|
||||
emojiSuggestor () {
|
||||
|
@ -157,6 +161,12 @@ const PostStatusForm = {
|
|||
safeDMEnabled () {
|
||||
return this.$store.state.instance.safeDM
|
||||
},
|
||||
stickersAvailable () {
|
||||
if (this.$store.state.instance.stickers) {
|
||||
return this.$store.state.instance.stickers.length > 0
|
||||
}
|
||||
return 0
|
||||
},
|
||||
pollsAvailable () {
|
||||
return this.$store.state.instance.pollsAvailable &&
|
||||
this.$store.state.instance.pollLimits.max_options >= 2
|
||||
|
@ -212,6 +222,7 @@ const PostStatusForm = {
|
|||
poll: {}
|
||||
}
|
||||
this.pollFormVisible = false
|
||||
this.stickerPickerVisible = false
|
||||
this.$refs.mediaUpload.clearFile()
|
||||
this.clearPollForm()
|
||||
this.$emit('posted')
|
||||
|
@ -228,6 +239,7 @@ const PostStatusForm = {
|
|||
addMediaFile (fileInfo) {
|
||||
this.newStatus.files.push(fileInfo)
|
||||
this.enableSubmit()
|
||||
this.stickerPickerVisible = false
|
||||
},
|
||||
removeMediaFile (fileInfo) {
|
||||
let index = this.newStatus.files.indexOf(fileInfo)
|
||||
|
@ -287,6 +299,14 @@ const PostStatusForm = {
|
|||
changeVis (visibility) {
|
||||
this.newStatus.visibility = visibility
|
||||
},
|
||||
toggleStickerPicker () {
|
||||
this.stickerPickerVisible = !this.stickerPickerVisible
|
||||
},
|
||||
clearStickerPicker () {
|
||||
if (this.$refs.stickerPicker) {
|
||||
this.$refs.stickerPicker.clear()
|
||||
}
|
||||
},
|
||||
togglePollForm () {
|
||||
this.pollFormVisible = !this.pollFormVisible
|
||||
},
|
||||
|
|
|
@ -157,6 +157,17 @@
|
|||
@uploaded="addMediaFile"
|
||||
@upload-failed="uploadFailed"
|
||||
/>
|
||||
<div
|
||||
v-if="stickersAvailable"
|
||||
class="sticker-icon"
|
||||
>
|
||||
<i
|
||||
:title="$t('stickers.add_sticker')"
|
||||
class="icon-picture btn btn-default"
|
||||
:class="{ selected: stickerPickerVisible }"
|
||||
@click="toggleStickerPicker"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="pollsAvailable"
|
||||
class="poll-icon"
|
||||
|
@ -169,7 +180,6 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="posting"
|
||||
disabled
|
||||
|
@ -248,6 +258,11 @@
|
|||
<label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label>
|
||||
</div>
|
||||
</form>
|
||||
<sticker-picker
|
||||
v-if="stickerPickerVisible"
|
||||
ref="stickerPicker"
|
||||
@uploaded="addMediaFile"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -310,7 +325,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.poll-icon {
|
||||
.poll-icon, .sticker-icon {
|
||||
font-size: 26px;
|
||||
flex: 1;
|
||||
|
||||
|
@ -320,6 +335,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.sticker-icon {
|
||||
flex: 0;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.icon-chart-bar {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
:disabled="progress || disabled"
|
||||
@click="onClick"
|
||||
>
|
||||
<template v-if="progress">
|
||||
<template v-if="progress && $slots.progress">
|
||||
<slot name="progress" />
|
||||
</template>
|
||||
<template v-else>
|
||||
|
|
98
src/components/search/search.js
Normal file
98
src/components/search/search.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
import FollowCard from '../follow_card/follow_card.vue'
|
||||
import Conversation from '../conversation/conversation.vue'
|
||||
import Status from '../status/status.vue'
|
||||
import map from 'lodash/map'
|
||||
|
||||
const Search = {
|
||||
components: {
|
||||
FollowCard,
|
||||
Conversation,
|
||||
Status
|
||||
},
|
||||
props: [
|
||||
'query'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
loaded: false,
|
||||
loading: false,
|
||||
searchTerm: this.query || '',
|
||||
userIds: [],
|
||||
statuses: [],
|
||||
hashtags: [],
|
||||
currenResultTab: 'statuses'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
users () {
|
||||
return this.userIds.map(userId => this.$store.getters.findUser(userId))
|
||||
},
|
||||
visibleStatuses () {
|
||||
const allStatusesObject = this.$store.state.statuses.allStatusesObject
|
||||
|
||||
return this.statuses.filter(status =>
|
||||
allStatusesObject[status.id] && !allStatusesObject[status.id].deleted
|
||||
)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.search(this.query)
|
||||
},
|
||||
watch: {
|
||||
query (newValue) {
|
||||
this.searchTerm = newValue
|
||||
this.search(newValue)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
newQuery (query) {
|
||||
this.$router.push({ name: 'search', query: { query } })
|
||||
this.$refs.searchInput.focus()
|
||||
},
|
||||
search (query) {
|
||||
if (!query) {
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
this.userIds = []
|
||||
this.statuses = []
|
||||
this.hashtags = []
|
||||
this.$refs.searchInput.blur()
|
||||
|
||||
this.$store.dispatch('search', { q: query, resolve: true })
|
||||
.then(data => {
|
||||
this.loading = false
|
||||
this.userIds = map(data.accounts, 'id')
|
||||
this.statuses = data.statuses
|
||||
this.hashtags = data.hashtags
|
||||
this.currenResultTab = this.getActiveTab()
|
||||
this.loaded = true
|
||||
})
|
||||
},
|
||||
resultCount (tabName) {
|
||||
const length = this[tabName].length
|
||||
return length === 0 ? '' : ` (${length})`
|
||||
},
|
||||
onResultTabSwitch (_index, dataset) {
|
||||
this.currenResultTab = dataset.filter
|
||||
},
|
||||
getActiveTab () {
|
||||
if (this.visibleStatuses.length > 0) {
|
||||
return 'statuses'
|
||||
} else if (this.users.length > 0) {
|
||||
return 'people'
|
||||
} else if (this.hashtags.length > 0) {
|
||||
return 'hashtags'
|
||||
}
|
||||
|
||||
return 'statuses'
|
||||
},
|
||||
lastHistoryRecord (hashtag) {
|
||||
return hashtag.history && hashtag.history[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Search
|
211
src/components/search/search.vue
Normal file
211
src/components/search/search.vue
Normal file
|
@ -0,0 +1,211 @@
|
|||
<template>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="title">
|
||||
{{ $t('nav.search') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-input-container">
|
||||
<input
|
||||
ref="searchInput"
|
||||
v-model="searchTerm"
|
||||
class="search-input"
|
||||
:placeholder="$t('nav.search')"
|
||||
@keyup.enter="newQuery(searchTerm)"
|
||||
>
|
||||
<button
|
||||
class="btn search-button"
|
||||
@click="newQuery(searchTerm)"
|
||||
>
|
||||
<i class="icon-search" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="text-center loading-icon"
|
||||
>
|
||||
<i class="icon-spin3 animate-spin" />
|
||||
</div>
|
||||
<div v-else-if="loaded">
|
||||
<div class="search-nav-heading">
|
||||
<tab-switcher
|
||||
ref="tabSwitcher"
|
||||
:on-switch="onResultTabSwitch"
|
||||
:custom-active="currenResultTab"
|
||||
>
|
||||
<span
|
||||
data-tab-dummy
|
||||
data-filter="statuses"
|
||||
:label="$t('user_card.statuses') + resultCount('visibleStatuses')"
|
||||
/>
|
||||
<span
|
||||
data-tab-dummy
|
||||
data-filter="people"
|
||||
:label="$t('search.people') + resultCount('users')"
|
||||
/>
|
||||
<span
|
||||
data-tab-dummy
|
||||
data-filter="hashtags"
|
||||
:label="$t('search.hashtags') + resultCount('hashtags')"
|
||||
/>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div v-if="currenResultTab === 'statuses'">
|
||||
<div
|
||||
v-if="visibleStatuses.length === 0 && !loading && loaded"
|
||||
class="search-result-heading"
|
||||
>
|
||||
<h4>{{ $t('search.no_results') }}</h4>
|
||||
</div>
|
||||
<Status
|
||||
v-for="status in visibleStatuses"
|
||||
:key="status.id"
|
||||
:collapsable="false"
|
||||
:expandable="false"
|
||||
:compact="false"
|
||||
class="search-result"
|
||||
:statusoid="status"
|
||||
:no-heading="false"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="currenResultTab === 'people'">
|
||||
<div
|
||||
v-if="users.length === 0 && !loading && loaded"
|
||||
class="search-result-heading"
|
||||
>
|
||||
<h4>{{ $t('search.no_results') }}</h4>
|
||||
</div>
|
||||
<FollowCard
|
||||
v-for="user in users"
|
||||
:key="user.id"
|
||||
:user="user"
|
||||
class="list-item search-result"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="currenResultTab === 'hashtags'">
|
||||
<div
|
||||
v-if="hashtags.length === 0 && !loading && loaded"
|
||||
class="search-result-heading"
|
||||
>
|
||||
<h4>{{ $t('search.no_results') }}</h4>
|
||||
</div>
|
||||
<div
|
||||
v-for="hashtag in hashtags"
|
||||
:key="hashtag.url"
|
||||
class="status trend search-result"
|
||||
>
|
||||
<div class="hashtag">
|
||||
<router-link :to="{ name: 'tag-timeline', params: { tag: hashtag.name } }">
|
||||
#{{ hashtag.name }}
|
||||
</router-link>
|
||||
<div v-if="lastHistoryRecord(hashtag)">
|
||||
<span v-if="lastHistoryRecord(hashtag).accounts == 1">
|
||||
{{ $t('search.person_talking', { count: lastHistoryRecord(hashtag).accounts }) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('search.people_talking', { count: lastHistoryRecord(hashtag).accounts }) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="lastHistoryRecord(hashtag)"
|
||||
class="count"
|
||||
>
|
||||
{{ lastHistoryRecord(hashtag).uses }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-result-footer text-center panel-footer faint" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./search.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.search-result-heading {
|
||||
color: $fallback--faint;
|
||||
color: var(--faint, $fallback--faint);
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media all and (max-width: 800px) {
|
||||
.search-nav-heading {
|
||||
.tab-switcher .tabs .tab-wrapper {
|
||||
display: block;
|
||||
justify-content: center;
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-result {
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid;
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
.search-result-footer {
|
||||
border-width: 1px 0 0 0;
|
||||
border-style: solid;
|
||||
border-color: var(--border, $fallback--border);
|
||||
padding: 10px;
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--panel, $fallback--fg);
|
||||
}
|
||||
|
||||
.search-input-container {
|
||||
padding: 0.8rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
line-height: 1.125rem;
|
||||
font-size: 1rem;
|
||||
padding: 0.5rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.hashtag {
|
||||
flex: 1 1 auto;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.count {
|
||||
flex: 0 0 auto;
|
||||
width: 2rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: 2.25rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
27
src/components/search_bar/search_bar.js
Normal file
27
src/components/search_bar/search_bar.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
const SearchBar = {
|
||||
data: () => ({
|
||||
searchTerm: undefined,
|
||||
hidden: true,
|
||||
error: false,
|
||||
loading: false
|
||||
}),
|
||||
watch: {
|
||||
'$route': function (route) {
|
||||
if (route.name === 'search') {
|
||||
this.searchTerm = route.query.query
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
find (searchTerm) {
|
||||
this.$router.push({ name: 'search', query: { query: searchTerm } })
|
||||
this.$refs.searchInput.focus()
|
||||
},
|
||||
toggleHidden () {
|
||||
this.hidden = !this.hidden
|
||||
this.$emit('toggled', this.hidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default SearchBar
|
|
@ -1,36 +1,36 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="user-finder-container">
|
||||
<div class="search-bar-container">
|
||||
<i
|
||||
v-if="loading"
|
||||
class="icon-spin4 user-finder-icon animate-spin-slow"
|
||||
class="icon-spin4 finder-icon animate-spin-slow"
|
||||
/>
|
||||
<a
|
||||
v-if="hidden"
|
||||
href="#"
|
||||
:title="$t('finder.find_user')"
|
||||
:title="$t('nav.search')"
|
||||
><i
|
||||
class="icon-user-plus user-finder-icon"
|
||||
class="button-icon icon-search"
|
||||
@click.prevent.stop="toggleHidden"
|
||||
/></a>
|
||||
<template v-else>
|
||||
<input
|
||||
id="user-finder-input"
|
||||
ref="userSearchInput"
|
||||
v-model="username"
|
||||
class="user-finder-input"
|
||||
:placeholder="$t('finder.find_user')"
|
||||
id="search-bar-input"
|
||||
ref="searchInput"
|
||||
v-model="searchTerm"
|
||||
class="search-bar-input"
|
||||
:placeholder="$t('nav.search')"
|
||||
type="text"
|
||||
@keyup.enter="findUser(username)"
|
||||
@keyup.enter="find(searchTerm)"
|
||||
>
|
||||
<button
|
||||
class="btn search-button"
|
||||
@click="findUser(username)"
|
||||
@click="find(searchTerm)"
|
||||
>
|
||||
<i class="icon-search" />
|
||||
</button>
|
||||
<i
|
||||
class="button-icon icon-cancel user-finder-icon"
|
||||
class="button-icon icon-cancel"
|
||||
@click.prevent.stop="toggleHidden"
|
||||
/>
|
||||
</template>
|
||||
|
@ -38,22 +38,24 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./user_finder.js"></script>
|
||||
<script src="./search_bar.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.user-finder-container {
|
||||
.search-bar-container {
|
||||
max-width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
vertical-align: baseline;
|
||||
justify-content: flex-end;
|
||||
|
||||
.user-finder-input,
|
||||
.search-bar-input,
|
||||
.search-button {
|
||||
height: 29px;
|
||||
}
|
||||
.user-finder-input {
|
||||
|
||||
.search-bar-input {
|
||||
// TODO: do this properly without a rough guesstimate of 2 icons + paddings
|
||||
max-width: calc(100% - 30px - 30px - 20px);
|
||||
}
|
||||
|
@ -62,6 +64,10 @@
|
|||
margin-left: .5em;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.icon-cancel {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -100,8 +100,8 @@
|
|||
</ul>
|
||||
<ul>
|
||||
<li @click="toggleDrawer">
|
||||
<router-link :to="{ name: 'user-search' }">
|
||||
{{ $t("nav.user_search") }}
|
||||
<router-link :to="{ name: 'search' }">
|
||||
{{ $t("nav.search") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li
|
||||
|
|
|
@ -110,8 +110,9 @@ const Status = {
|
|||
},
|
||||
muteWordHits () {
|
||||
const statusText = this.status.text.toLowerCase()
|
||||
const statusSummary = this.status.summary.toLowerCase()
|
||||
const hits = filter(this.muteWords, (muteWord) => {
|
||||
return statusText.includes(muteWord.toLowerCase())
|
||||
return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())
|
||||
})
|
||||
|
||||
return hits
|
||||
|
@ -280,6 +281,11 @@ const Status = {
|
|||
},
|
||||
tags () {
|
||||
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
|
||||
},
|
||||
hidePostStats () {
|
||||
return typeof this.$store.state.config.hidePostStats === 'undefined'
|
||||
? this.$store.state.instance.hidePostStats
|
||||
: this.$store.state.config.hidePostStats
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -316,11 +322,8 @@ const Status = {
|
|||
this.error = undefined
|
||||
},
|
||||
linkClicked (event) {
|
||||
let { target } = event
|
||||
if (target.tagName === 'SPAN') {
|
||||
target = target.parentNode
|
||||
}
|
||||
if (target.tagName === 'A') {
|
||||
const target = event.target.closest('.status-content a')
|
||||
if (target) {
|
||||
if (target.className.match(/mention/)) {
|
||||
const href = target.href
|
||||
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
||||
|
@ -419,6 +422,18 @@ const Status = {
|
|||
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
|
||||
}
|
||||
}
|
||||
},
|
||||
'status.repeat_num': function (num) {
|
||||
// refetch repeats when repeat_num is changed in any way
|
||||
if (this.isFocused && this.statusFromGlobalRepository.rebloggedBy && this.statusFromGlobalRepository.rebloggedBy.length !== num) {
|
||||
this.$store.dispatch('fetchRepeats', this.status.id)
|
||||
}
|
||||
},
|
||||
'status.fave_num': function (num) {
|
||||
// refetch favs when fave_num is changed in any way
|
||||
if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) {
|
||||
this.$store.dispatch('fetchFavs', this.status.id)
|
||||
}
|
||||
}
|
||||
},
|
||||
filters: {
|
||||
|
|
|
@ -344,7 +344,7 @@
|
|||
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="isFocused && combinedFavsAndRepeatsUsers.length > 0"
|
||||
v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0"
|
||||
class="favs-repeated-users"
|
||||
>
|
||||
<div class="stats">
|
||||
|
@ -820,11 +820,12 @@ $status-margin: 0.75em;
|
|||
}
|
||||
|
||||
.status-actions {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
margin-top: $status-margin;
|
||||
|
||||
div, favorite-button {
|
||||
> * {
|
||||
max-width: 4em;
|
||||
flex: 1;
|
||||
}
|
||||
|
|
52
src/components/sticker_picker/sticker_picker.js
Normal file
52
src/components/sticker_picker/sticker_picker.js
Normal file
|
@ -0,0 +1,52 @@
|
|||
/* eslint-env browser */
|
||||
import statusPosterService from '../../services/status_poster/status_poster.service.js'
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||
|
||||
const StickerPicker = {
|
||||
components: [
|
||||
TabSwitcher
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
meta: {
|
||||
stickers: []
|
||||
},
|
||||
path: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pack () {
|
||||
return this.$store.state.instance.stickers || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clear () {
|
||||
this.meta = {
|
||||
stickers: []
|
||||
}
|
||||
},
|
||||
pick (sticker, name) {
|
||||
const store = this.$store
|
||||
// TODO remove this workaround by finding a way to bypass reuploads
|
||||
fetch(sticker)
|
||||
.then((res) => {
|
||||
res.blob().then((blob) => {
|
||||
var file = new File([blob], name, { mimetype: 'image/png' })
|
||||
var formData = new FormData()
|
||||
formData.append('file', file)
|
||||
statusPosterService.uploadMedia({ store, formData })
|
||||
.then((fileData) => {
|
||||
this.$emit('uploaded', fileData)
|
||||
this.clear()
|
||||
}, (error) => {
|
||||
console.warn("Can't attach sticker")
|
||||
console.warn(error)
|
||||
this.$emit('upload-failed', 'default')
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StickerPicker
|
62
src/components/sticker_picker/sticker_picker.vue
Normal file
62
src/components/sticker_picker/sticker_picker.vue
Normal file
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<div
|
||||
class="sticker-picker"
|
||||
>
|
||||
<div
|
||||
class="sticker-picker-panel"
|
||||
>
|
||||
<tab-switcher
|
||||
:render-only-focused="true"
|
||||
>
|
||||
<div
|
||||
v-for="stickerpack in pack"
|
||||
:key="stickerpack.path"
|
||||
:image-tooltip="stickerpack.meta.title"
|
||||
:image="stickerpack.path + stickerpack.meta.tabIcon"
|
||||
class="sticker-picker-content"
|
||||
>
|
||||
<div
|
||||
v-for="sticker in stickerpack.meta.stickers"
|
||||
:key="sticker"
|
||||
class="sticker"
|
||||
@click="pick(stickerpack.path + sticker, stickerpack.meta.title)"
|
||||
>
|
||||
<img
|
||||
:src="stickerpack.path + sticker"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./sticker_picker.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.sticker-picker {
|
||||
.sticker-picker-panel {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
.sticker-picker-content {
|
||||
max-height: 300px;
|
||||
overflow-y: scroll;
|
||||
overflow-x: auto;
|
||||
.sticker {
|
||||
display: inline-block;
|
||||
width: 20%;
|
||||
height: 20%;
|
||||
img {
|
||||
width: 100%;
|
||||
&:hover {
|
||||
filter: drop-shadow(0 0 5px var(--link, $fallback--link));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -4,7 +4,7 @@ import './tab_switcher.scss'
|
|||
|
||||
export default Vue.component('tab-switcher', {
|
||||
name: 'TabSwitcher',
|
||||
props: ['renderOnlyFocused', 'onSwitch'],
|
||||
props: ['renderOnlyFocused', 'onSwitch', 'customActive'],
|
||||
data () {
|
||||
return {
|
||||
active: this.$slots.default.findIndex(_ => _.tag)
|
||||
|
@ -24,6 +24,14 @@ export default Vue.component('tab-switcher', {
|
|||
}
|
||||
this.active = index
|
||||
}
|
||||
},
|
||||
isActiveTab (index) {
|
||||
const customActiveIndex = this.$slots.default.findIndex(slot => {
|
||||
const dataFilter = slot.data && slot.data.attrs && slot.data.attrs['data-filter']
|
||||
return this.customActive && this.customActive === dataFilter
|
||||
})
|
||||
|
||||
return customActiveIndex > -1 ? customActiveIndex === index : index === this.active
|
||||
}
|
||||
},
|
||||
render (h) {
|
||||
|
@ -33,11 +41,23 @@ export default Vue.component('tab-switcher', {
|
|||
const classesTab = ['tab']
|
||||
const classesWrapper = ['tab-wrapper']
|
||||
|
||||
if (index === this.active) {
|
||||
if (this.isActiveTab(index)) {
|
||||
classesTab.push('active')
|
||||
classesWrapper.push('active')
|
||||
}
|
||||
|
||||
if (slot.data.attrs.image) {
|
||||
return (
|
||||
<div class={ classesWrapper.join(' ')}>
|
||||
<button
|
||||
disabled={slot.data.attrs.disabled}
|
||||
onClick={this.activateTab(index)}
|
||||
class={classesTab.join(' ')}>
|
||||
<img src={slot.data.attrs.image} title={slot.data.attrs['image-tooltip']}/>
|
||||
{slot.data.attrs.label ? '' : slot.data.attrs.label}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div class={ classesWrapper.join(' ')}>
|
||||
<button
|
||||
|
|
|
@ -53,6 +53,12 @@
|
|||
background: transparent;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
img {
|
||||
max-height: 26px;
|
||||
vertical-align: top;
|
||||
margin-top: -5px;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import RemoteFollow from '../remote_follow/remote_follow.vue'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import ModerationTools from '../moderation_tools/moderation_tools.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||
|
@ -104,7 +105,8 @@ export default {
|
|||
components: {
|
||||
UserAvatar,
|
||||
RemoteFollow,
|
||||
ModerationTools
|
||||
ModerationTools,
|
||||
ProgressButton
|
||||
},
|
||||
methods: {
|
||||
followUser () {
|
||||
|
@ -135,6 +137,12 @@ export default {
|
|||
unmuteUser () {
|
||||
this.$store.dispatch('unmuteUser', this.user.id)
|
||||
},
|
||||
subscribeUser () {
|
||||
return this.$store.dispatch('subscribeUser', this.user.id)
|
||||
},
|
||||
unsubscribeUser () {
|
||||
return this.$store.dispatch('unsubscribeUser', this.user.id)
|
||||
},
|
||||
setProfileView (v) {
|
||||
if (this.switcher) {
|
||||
const store = this.$store
|
||||
|
|
|
@ -112,101 +112,120 @@
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isOtherUser"
|
||||
v-if="loggedIn && isOtherUser"
|
||||
class="user-interactions"
|
||||
>
|
||||
<div
|
||||
v-if="loggedIn"
|
||||
class="follow"
|
||||
>
|
||||
<span v-if="user.following">
|
||||
<!--Following them!-->
|
||||
<button
|
||||
class="pressed"
|
||||
:disabled="followRequestInProgress"
|
||||
:title="$t('user_card.follow_unfollow')"
|
||||
@click="unfollowUser"
|
||||
>
|
||||
<template v-if="followRequestInProgress">
|
||||
{{ $t('user_card.follow_progress') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.following') }}
|
||||
</template>
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="!user.following">
|
||||
<button
|
||||
:disabled="followRequestInProgress"
|
||||
:title="followRequestSent ? $t('user_card.follow_again') : ''"
|
||||
@click="followUser"
|
||||
>
|
||||
<template v-if="followRequestInProgress">
|
||||
{{ $t('user_card.follow_progress') }}
|
||||
</template>
|
||||
<template v-else-if="followRequestSent">
|
||||
{{ $t('user_card.follow_sent') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.follow') }}
|
||||
</template>
|
||||
</button>
|
||||
</span>
|
||||
<div v-if="!user.following">
|
||||
<button
|
||||
class="btn btn-default btn-block"
|
||||
:disabled="followRequestInProgress"
|
||||
:title="followRequestSent ? $t('user_card.follow_again') : ''"
|
||||
@click="followUser"
|
||||
>
|
||||
<template v-if="followRequestInProgress">
|
||||
{{ $t('user_card.follow_progress') }}
|
||||
</template>
|
||||
<template v-else-if="followRequestSent">
|
||||
{{ $t('user_card.follow_sent') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('user_card.follow') }}
|
||||
</template>
|
||||
</button>
|
||||
</div>
|
||||
<div v-else-if="followRequestInProgress">
|
||||
<button
|
||||
class="btn btn-default btn-block pressed"
|
||||
disabled
|
||||
:title="$t('user_card.follow_unfollow')"
|
||||
@click="unfollowUser"
|
||||
>
|
||||
{{ $t('user_card.follow_progress') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="isOtherUser && loggedIn"
|
||||
class="mute"
|
||||
v-else
|
||||
class="btn-group"
|
||||
>
|
||||
<span v-if="user.muted">
|
||||
<button
|
||||
class="pressed"
|
||||
@click="unmuteUser"
|
||||
>
|
||||
{{ $t('user_card.muted') }}
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="!user.muted">
|
||||
<button @click="muteUser">
|
||||
{{ $t('user_card.mute') }}
|
||||
</button>
|
||||
</span>
|
||||
<button
|
||||
class="btn btn-default pressed"
|
||||
:title="$t('user_card.follow_unfollow')"
|
||||
@click="unfollowUser"
|
||||
>
|
||||
{{ $t('user_card.following') }}
|
||||
</button>
|
||||
<ProgressButton
|
||||
v-if="!user.subscribed"
|
||||
class="btn btn-default"
|
||||
:click="subscribeUser"
|
||||
:title="$t('user_card.subscribe')"
|
||||
>
|
||||
<i class="icon-bell-alt" />
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-else
|
||||
class="btn btn-default pressed"
|
||||
:click="unsubscribeUser"
|
||||
:title="$t('user_card.unsubscribe')"
|
||||
>
|
||||
<i class="icon-bell-ringing-o" />
|
||||
</ProgressButton>
|
||||
</div>
|
||||
<div v-if="!loggedIn && user.is_local">
|
||||
<RemoteFollow :user="user" />
|
||||
|
||||
<div>
|
||||
<button
|
||||
v-if="user.muted"
|
||||
class="btn btn-default btn-block pressed"
|
||||
@click="unmuteUser"
|
||||
>
|
||||
{{ $t('user_card.muted') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-default btn-block"
|
||||
@click="muteUser"
|
||||
>
|
||||
{{ $t('user_card.mute') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="isOtherUser && loggedIn"
|
||||
class="block"
|
||||
>
|
||||
<span v-if="user.statusnet_blocking">
|
||||
<button
|
||||
class="pressed"
|
||||
@click="unblockUser"
|
||||
>
|
||||
{{ $t('user_card.blocked') }}
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="!user.statusnet_blocking">
|
||||
<button @click="blockUser">
|
||||
{{ $t('user_card.block') }}
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<button
|
||||
v-if="user.statusnet_blocking"
|
||||
class="btn btn-default btn-block pressed"
|
||||
@click="unblockUser"
|
||||
>
|
||||
{{ $t('user_card.blocked') }}
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="btn btn-default btn-block"
|
||||
@click="blockUser"
|
||||
>
|
||||
{{ $t('user_card.block') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="isOtherUser && loggedIn"
|
||||
class="block"
|
||||
>
|
||||
<span>
|
||||
<button @click="reportUser">
|
||||
{{ $t('user_card.report') }}
|
||||
</button>
|
||||
</span>
|
||||
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-default btn-block"
|
||||
@click="reportUser"
|
||||
>
|
||||
{{ $t('user_card.report') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ModerationTools
|
||||
v-if="loggedIn.role === "admin""
|
||||
:user="user"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="!loggedIn && user.is_local"
|
||||
class="user-interactions"
|
||||
>
|
||||
<RemoteFollow :user="user" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
@ -264,7 +283,6 @@
|
|||
|
||||
.user-card {
|
||||
background-size: cover;
|
||||
overflow: hidden;
|
||||
|
||||
.panel-heading {
|
||||
padding: .5em 0;
|
||||
|
@ -279,6 +297,8 @@
|
|||
word-wrap: break-word;
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%);
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
|
||||
border-bottom-right-radius: inherit;
|
||||
border-bottom-left-radius: inherit;
|
||||
}
|
||||
|
||||
p {
|
||||
|
@ -484,43 +504,26 @@
|
|||
}
|
||||
}
|
||||
.user-interactions {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-right: -.75em;
|
||||
|
||||
div {
|
||||
> * {
|
||||
flex: 1 0 0;
|
||||
margin-right: .75em;
|
||||
margin-bottom: .6em;
|
||||
margin: 0 .75em .6em 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mute {
|
||||
max-width: 220px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.follow {
|
||||
max-width: 220px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.remote-button {
|
||||
height: 28px !important;
|
||||
width: 92%;
|
||||
}
|
||||
|
||||
.pressed {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.2);
|
||||
border-top-color: rgba(0, 0, 0, 0.2);
|
||||
&.pressed {
|
||||
// TODO: This should be themed.
|
||||
border-bottom-color: rgba(255, 255, 255, 0.2);
|
||||
border-top-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
const UserFinder = {
|
||||
data: () => ({
|
||||
username: undefined,
|
||||
hidden: true,
|
||||
error: false,
|
||||
loading: false
|
||||
}),
|
||||
methods: {
|
||||
findUser (username) {
|
||||
this.$router.push({ name: 'user-search', query: { query: username } })
|
||||
this.$refs.userSearchInput.focus()
|
||||
},
|
||||
toggleHidden () {
|
||||
this.hidden = !this.hidden
|
||||
this.$emit('toggled', this.hidden)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UserFinder
|
|
@ -3,7 +3,6 @@ import UserCard from '../user_card/user_card.vue'
|
|||
import FollowCard from '../follow_card/follow_card.vue'
|
||||
import Timeline from '../timeline/timeline.vue'
|
||||
import Conversation from '../conversation/conversation.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'
|
||||
|
||||
|
@ -132,7 +131,6 @@ const UserProfile = {
|
|||
Timeline,
|
||||
FollowerList,
|
||||
FriendList,
|
||||
ModerationTools,
|
||||
FollowCard,
|
||||
Conversation
|
||||
}
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import FollowCard from '../follow_card/follow_card.vue'
|
||||
import map from 'lodash/map'
|
||||
|
||||
const userSearch = {
|
||||
components: {
|
||||
FollowCard
|
||||
},
|
||||
props: [
|
||||
'query'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
username: '',
|
||||
userIds: [],
|
||||
loading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
users () {
|
||||
return this.userIds.map(userId => this.$store.getters.findUser(userId))
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.search(this.query)
|
||||
},
|
||||
watch: {
|
||||
query (newV) {
|
||||
this.search(newV)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
newQuery (query) {
|
||||
this.$router.push({ name: 'user-search', query: { query } })
|
||||
this.$refs.userSearchInput.focus()
|
||||
},
|
||||
search (query) {
|
||||
if (!query) {
|
||||
this.users = []
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
this.$store.dispatch('searchUsers', query)
|
||||
.then((res) => {
|
||||
this.loading = false
|
||||
this.userIds = map(res, 'id')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default userSearch
|
|
@ -1,57 +0,0 @@
|
|||
<template>
|
||||
<div class="user-search panel panel-default">
|
||||
<div class="panel-heading">
|
||||
{{ $t('nav.user_search') }}
|
||||
</div>
|
||||
<div class="user-search-input-container">
|
||||
<input
|
||||
ref="userSearchInput"
|
||||
v-model="username"
|
||||
class="user-finder-input"
|
||||
:placeholder="$t('finder.find_user')"
|
||||
@keyup.enter="newQuery(username)"
|
||||
>
|
||||
<button
|
||||
class="btn search-button"
|
||||
@click="newQuery(username)"
|
||||
>
|
||||
<i class="icon-search" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-if="loading"
|
||||
class="text-center loading-icon"
|
||||
>
|
||||
<i class="icon-spin3 animate-spin" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="panel-body"
|
||||
>
|
||||
<FollowCard
|
||||
v-for="user in users"
|
||||
:key="user.id"
|
||||
:user="user"
|
||||
class="list-item"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./user_search.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.user-search-input-container {
|
||||
margin: 0.5em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.search-button {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
padding: 1em;
|
||||
}
|
||||
</style>
|
|
@ -17,7 +17,6 @@ import Autosuggest from '../autosuggest/autosuggest.vue'
|
|||
import Importer from '../importer/importer.vue'
|
||||
import Exporter from '../exporter/exporter.vue'
|
||||
import withSubscription from '../../hocs/with_subscription/with_subscription'
|
||||
import userSearchApi from '../../services/new_api/user_search.js'
|
||||
import Mfa from './mfa.vue'
|
||||
|
||||
const BlockList = withSubscription({
|
||||
|
@ -92,7 +91,8 @@ const UserSettings = {
|
|||
...this.$store.state.instance.emoji,
|
||||
...this.$store.state.instance.customEmoji
|
||||
],
|
||||
users: this.$store.state.users.users
|
||||
users: this.$store.state.users.users,
|
||||
updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
|
||||
})
|
||||
},
|
||||
emojiSuggestor () {
|
||||
|
@ -322,11 +322,8 @@ const UserSettings = {
|
|||
})
|
||||
},
|
||||
queryUserIds (query) {
|
||||
return userSearchApi.search({ query, store: this.$store })
|
||||
.then((users) => {
|
||||
this.$store.dispatch('addNewUsers', users)
|
||||
return map(users, 'id')
|
||||
})
|
||||
return this.$store.dispatch('searchUsers', query)
|
||||
.then((users) => map(users, 'id'))
|
||||
},
|
||||
blockUsers (ids) {
|
||||
return this.$store.dispatch('blockUsers', ids)
|
||||
|
|
|
@ -78,6 +78,7 @@
|
|||
"timeline": "Timeline",
|
||||
"twkn": "The Whole Known Network",
|
||||
"user_search": "User Search",
|
||||
"search": "Search",
|
||||
"who_to_follow": "Who to follow",
|
||||
"preferences": "Preferences"
|
||||
},
|
||||
|
@ -105,6 +106,9 @@
|
|||
"expired": "Poll ended {0} ago",
|
||||
"not_enough_options": "Too few unique options in poll"
|
||||
},
|
||||
"stickers": {
|
||||
"add_sticker": "Add Sticker"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "Repeats and Favorites",
|
||||
"follows": "New follows",
|
||||
|
@ -529,6 +533,8 @@
|
|||
"remote_follow": "Remote follow",
|
||||
"report": "Report",
|
||||
"statuses": "Statuses",
|
||||
"subscribe": "Subscribe",
|
||||
"unsubscribe": "Unsubscribe",
|
||||
"unblock": "Unblock",
|
||||
"unblock_progress": "Unblocking...",
|
||||
"block_progress": "Blocking...",
|
||||
|
@ -593,5 +599,12 @@
|
|||
"GiB": "GiB",
|
||||
"TiB": "TiB"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"people": "People",
|
||||
"hashtags": "Hashtags",
|
||||
"person_talking": "{count} person talking",
|
||||
"people_talking": "{count} people talking",
|
||||
"no_results": "No results"
|
||||
}
|
||||
}
|
||||
|
|
123
src/i18n/es.json
123
src/i18n/es.json
|
@ -27,7 +27,11 @@
|
|||
"optional": "opcional",
|
||||
"show_more": "Mostrar más",
|
||||
"show_less": "Mostrar menos",
|
||||
"cancel": "Cancelar"
|
||||
"cancel": "Cancelar",
|
||||
"disable": "Inhabilitar",
|
||||
"enable": "Habilitar",
|
||||
"confirm": "Confirmar",
|
||||
"verify": "Verificar"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Recortar la foto",
|
||||
|
@ -48,7 +52,15 @@
|
|||
"placeholder": "p.ej. lain",
|
||||
"register": "Registrar",
|
||||
"username": "Usuario",
|
||||
"hint": "Inicia sesión para unirte a la discusión"
|
||||
"hint": "Inicia sesión para unirte a la discusión",
|
||||
"authentication_code": "Código de autentificación",
|
||||
"enter_recovery_code": "Inserta el código de recuperación",
|
||||
"enter_two_factor_code": "Inserta el código de doble factor",
|
||||
"recovery_code": "Código de recuperación",
|
||||
"heading" : {
|
||||
"totp" : "Autentificación de doble factor",
|
||||
"recovery" : "Recuperación de doble factor"
|
||||
}
|
||||
},
|
||||
"media_modal": {
|
||||
"previous": "Anterior",
|
||||
|
@ -60,11 +72,13 @@
|
|||
"chat": "Chat Local",
|
||||
"friend_requests": "Solicitudes de amistad",
|
||||
"mentions": "Menciones",
|
||||
"interactions": "Interacciones",
|
||||
"dms": "Mensajes Directo",
|
||||
"public_tl": "Línea Temporal Pública",
|
||||
"timeline": "Línea Temporal",
|
||||
"twkn": "Toda La Red Conocida",
|
||||
"user_search": "Búsqueda de Usuarios",
|
||||
"search": "Buscar",
|
||||
"who_to_follow": "A quién seguir",
|
||||
"preferences": "Preferencias"
|
||||
},
|
||||
|
@ -78,6 +92,25 @@
|
|||
"repeated_you": "repite tu estado",
|
||||
"no_more_notifications": "No hay más notificaciones"
|
||||
},
|
||||
"polls": {
|
||||
"add_poll": "Añadir encuesta",
|
||||
"add_option": "Añadir opción",
|
||||
"option": "Opción",
|
||||
"votes": "votos",
|
||||
"vote": "Votar",
|
||||
"type": "Tipo de encuesta",
|
||||
"single_choice": "Elección única",
|
||||
"multiple_choices": "Múltiples elecciones",
|
||||
"expiry": "Tiempo de vida de la encuesta",
|
||||
"expires_in": "La encuensta termina en {0}",
|
||||
"expired": "La encuesta terminó hace {0}",
|
||||
"not_enough_options": "Muy pocas opciones únicas en la encuesta"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "Favoritos y Repetidos",
|
||||
"follows": "Nuevos seguidores",
|
||||
"load_older": "Cargar interacciones antiguas"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "Publicar un nuevo estado",
|
||||
"account_not_locked_warning": "Tu cuenta no está {0}. Cualquiera puede seguirte y leer las entradas para Solo-Seguidores.",
|
||||
|
@ -91,9 +124,14 @@
|
|||
},
|
||||
"content_warning": "Tema (opcional)",
|
||||
"default": "Acabo de aterrizar en L.A.",
|
||||
"direct_warning": "Esta publicación solo será visible para los usuarios mencionados.",
|
||||
"direct_warning_to_all": "Esta publicación será visible para todos los usarios mencionados.",
|
||||
"direct_warning_to_first_only": "Esta publicación solo será visible para los usuarios mencionados al comienzo del mensaje.",
|
||||
"posting": "Publicando",
|
||||
"scope_notice": {
|
||||
"public": "Esta publicación será visible para todo el mundo",
|
||||
"private": "Esta publicación solo será visible para tus seguidores.",
|
||||
"unlisted": "Esta publicación no será visible en la Línea Temporal Pública ni en Toda La Red Conocida"
|
||||
},
|
||||
"scope": {
|
||||
"direct": "Directo - Solo para los usuarios mencionados.",
|
||||
"private": "Solo-Seguidores - Solo tus seguidores leeran la publicación",
|
||||
|
@ -127,6 +165,29 @@
|
|||
},
|
||||
"settings": {
|
||||
"app_name": "Nombre de la aplicación",
|
||||
"security": "Seguridad",
|
||||
"enter_current_password_to_confirm": "Introduce la contraseña actual para confirmar tu identidad",
|
||||
"mfa": {
|
||||
"otp" : "OTP",
|
||||
"setup_otp" : "Configurar OTP",
|
||||
"wait_pre_setup_otp" : "preconfiguración OTP",
|
||||
"confirm_and_enable" : "Confirmar y habilitar OTP",
|
||||
"title": "Autentificación de Doble Factor",
|
||||
"generate_new_recovery_codes" : "Generar nuevos códigos de recuperación",
|
||||
"warning_of_generate_new_codes" : "Cuando generas nuevos códigos de recuperación, los antiguos dejarán de funcionar.",
|
||||
"recovery_codes" : "Códigos de recuperación.",
|
||||
"waiting_a_recovery_codes": "Recibiendo códigos de respaldo",
|
||||
"recovery_codes_warning" : "Anote los códigos o guárdelos en un lugar seguro, de lo contrario no los volverá a ver. Si pierde el acceso a su aplicación 2FA y los códigos de recuperación, su cuenta quedará bloqueada.",
|
||||
"authentication_methods" : "Métodos de autentificación",
|
||||
"scan": {
|
||||
"title": "Escanear",
|
||||
"desc": "Usando su aplicación de doble factor, escanee este código QR o ingrese la clave de texto:",
|
||||
"secret_code": "Clave"
|
||||
},
|
||||
"verify": {
|
||||
"desc": "Para habilitar la autenticación de doble factor, ingrese el código de su aplicación 2FA:"
|
||||
}
|
||||
},
|
||||
"attachmentRadius": "Adjuntos",
|
||||
"attachments": "Adjuntos",
|
||||
"autoload": "Activar carga automática al llegar al final de la página",
|
||||
|
@ -233,6 +294,7 @@
|
|||
"reply_visibility_all": "Mostrar todas las réplicas",
|
||||
"reply_visibility_following": "Solo mostrar réplicas para mí o usuarios a los que sigo",
|
||||
"reply_visibility_self": "Solo mostrar réplicas para mí",
|
||||
"autohide_floating_post_button": "Ocultar automáticamente el botón 'Nueva Publicación' (móvil)",
|
||||
"saving_err": "Error al guardar los ajustes",
|
||||
"saving_ok": "Ajustes guardados",
|
||||
"search_user_to_block": "Buscar usuarios a bloquear",
|
||||
|
@ -265,6 +327,13 @@
|
|||
"true": "sí"
|
||||
},
|
||||
"notifications": "Notificaciones",
|
||||
"notification_setting": "Recibir notificaciones de:",
|
||||
"notification_setting_follows": "Usuarios que sigues",
|
||||
"notification_setting_non_follows": "Usuarios que no sigues",
|
||||
"notification_setting_followers": "Usuarios que te siguen",
|
||||
"notification_setting_non_followers": "Usuarios que no te siguen",
|
||||
"notification_mutes": "Para dejar de recibir notificaciones de un usuario específico, siléncialo.",
|
||||
"notification_blocks": "El bloqueo de un usuario detiene todas las notificaciones y también las cancela.",
|
||||
"enable_web_push_notifications": "Habilitar las notificiaciones en el navegador",
|
||||
"style": {
|
||||
"switcher": {
|
||||
|
@ -381,6 +450,40 @@
|
|||
"frontend_version": "Versión del Frontend"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} día",
|
||||
"days": "{0} días",
|
||||
"day_short": "{0}d",
|
||||
"days_short": "{0}d",
|
||||
"hour": "{0} hora",
|
||||
"hours": "{0} horas",
|
||||
"hour_short": "{0}h",
|
||||
"hours_short": "{0}h",
|
||||
"in_future": "en {0}",
|
||||
"in_past": "hace {0}",
|
||||
"minute": "{0} minuto",
|
||||
"minutes": "{0} minutos",
|
||||
"minute_short": "{0}min",
|
||||
"minutes_short": "{0}min",
|
||||
"month": "{0} mes",
|
||||
"months": "{0} meses",
|
||||
"month_short": "{0}m",
|
||||
"months_short": "{0}m",
|
||||
"now": "justo ahora",
|
||||
"now_short": "ahora",
|
||||
"second": "{0} segundo",
|
||||
"seconds": "{0} segundos",
|
||||
"second_short": "{0}s",
|
||||
"seconds_short": "{0}s",
|
||||
"week": "{0} semana",
|
||||
"weeks": "{0} semana",
|
||||
"week_short": "{0}sem",
|
||||
"weeks_short": "{0}sem",
|
||||
"year": "{0} año",
|
||||
"years": "{0} años",
|
||||
"year_short": "{0}a",
|
||||
"years_short": "{0}a"
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "Colapsar",
|
||||
"conversation": "Conversación",
|
||||
|
@ -396,6 +499,11 @@
|
|||
"status": {
|
||||
"favorites": "Favoritos",
|
||||
"repeats": "Repetidos",
|
||||
"delete": "Eliminar publicación",
|
||||
"pin": "Fijar en tu perfil",
|
||||
"unpin": "Desclavar de tu perfil",
|
||||
"pinned": "Fijado",
|
||||
"delete_confirm": "¿Realmente quieres borrar la publicación?",
|
||||
"reply_to": "Responder a",
|
||||
"replies_list": "Respuestas:"
|
||||
},
|
||||
|
@ -422,6 +530,8 @@
|
|||
"remote_follow": "Seguir",
|
||||
"report": "Reportar",
|
||||
"statuses": "Estados",
|
||||
"subscribe": "Suscribirse",
|
||||
"unsubscribe": "Desuscribirse",
|
||||
"unblock": "Desbloquear",
|
||||
"unblock_progress": "Desbloqueando...",
|
||||
"block_progress": "Bloqueando...",
|
||||
|
@ -486,5 +596,12 @@
|
|||
"GiB": "GiB",
|
||||
"TiB": "TiB"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"people": "Personas",
|
||||
"hashtags": "Hashtags",
|
||||
"person_talking": "{count} personas hablando",
|
||||
"people_talking": "{count} gente hablando",
|
||||
"no_results": "Sin resultados"
|
||||
}
|
||||
}
|
|
@ -27,7 +27,11 @@
|
|||
"optional": "かかなくてもよい",
|
||||
"show_more": "つづきをみる",
|
||||
"show_less": "たたむ",
|
||||
"cancel": "キャンセル"
|
||||
"cancel": "キャンセル",
|
||||
"disable": "なし",
|
||||
"enable": "あり",
|
||||
"confirm": "たしかめる",
|
||||
"verify": "たしかめる"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "がぞうをきりぬく",
|
||||
|
@ -48,7 +52,15 @@
|
|||
"placeholder": "れい: lain",
|
||||
"register": "はじめる",
|
||||
"username": "ユーザーめい",
|
||||
"hint": "はなしあいにくわわるには、ログインしてください"
|
||||
"hint": "はなしあいにくわわるには、ログインしてください",
|
||||
"authentication_code": "にんしょうコード",
|
||||
"enter_recovery_code": "リカバリーコードをいれてください",
|
||||
"enter_two_factor_code": "2-ファクターコードをいれてください",
|
||||
"recovery_code": "リカバリーコード",
|
||||
"heading" : {
|
||||
"totp" : "2-ファクターにんしょう",
|
||||
"recovery" : "2-ファクターリカバリー"
|
||||
}
|
||||
},
|
||||
"media_modal": {
|
||||
"previous": "まえ",
|
||||
|
@ -79,6 +91,20 @@
|
|||
"repeated_you": "あなたのステータスがリピートされました",
|
||||
"no_more_notifications": "つうちはありません"
|
||||
},
|
||||
"polls": {
|
||||
"add_poll": "いれふだをはじめる",
|
||||
"add_option": "オプションをふやす",
|
||||
"option": "オプション",
|
||||
"votes": "いれふだ",
|
||||
"vote": "ふだをいれる",
|
||||
"type": "いれふだのかた",
|
||||
"single_choice": "ひとつえらぶ",
|
||||
"multiple_choices": "いくつでもえらべる",
|
||||
"expiry": "いれふだのながさ",
|
||||
"expires_in": "いれふだは {0} で、おわります",
|
||||
"expired": "いれふだは {0} まえに、おわりました",
|
||||
"not_enough_options": "ユニークなオプションが、たりません"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "リピートとおきにいり",
|
||||
"follows": "あたらしいフォロー",
|
||||
|
@ -139,6 +165,29 @@
|
|||
},
|
||||
"settings": {
|
||||
"app_name": "アプリのなまえ",
|
||||
"security": "セキュリティ",
|
||||
"enter_current_password_to_confirm": "あなたのアイデンティティをたしかめるため、あなたのいまのパスワードをかいてください",
|
||||
"mfa": {
|
||||
"otp" : "OTP",
|
||||
"setup_otp" : "OTPをつくる",
|
||||
"wait_pre_setup_otp" : "OTPをよういしています",
|
||||
"confirm_and_enable" : "OTPをたしかめて、ゆうこうにする",
|
||||
"title": "2-ファクターにんしょう",
|
||||
"generate_new_recovery_codes" : "あたらしいリカバリーコードをつくる",
|
||||
"warning_of_generate_new_codes" : "あたらしいリカバリーコードをつくったら、ふるいコードはつかえなくなります。",
|
||||
"recovery_codes" : "リカバリーコード。",
|
||||
"waiting_a_recovery_codes": "バックアップコードをうけとっています...",
|
||||
"recovery_codes_warning" : "コードをかきうつすか、ひとにみられないところにセーブしてください。そうでなければ、あなたはこのコードをふたたびみることはできません。もしあなたが、2FAアプリのアクセスをうしなって、なおかつ、リカバリーコードもおもいだせないならば、あなたはあなたのアカウントから、しめだされます。",
|
||||
"authentication_methods" : "にんしょうメソッド",
|
||||
"scan": {
|
||||
"title": "スキャン",
|
||||
"desc": "あなたの2-ファクターアプリをつかって、このQRコードをスキャンするか、テキストキーをうちこんでください:",
|
||||
"secret_code": "キー"
|
||||
},
|
||||
"verify": {
|
||||
"desc": "2-ファクターにんしょうをつかうには、あなたの2-ファクターアプリのコードをいれてください:"
|
||||
}
|
||||
},
|
||||
"attachmentRadius": "ファイル",
|
||||
"attachments": "ファイル",
|
||||
"autoload": "したにスクロールしたとき、じどうてきによみこむ。",
|
||||
|
|
|
@ -27,7 +27,11 @@
|
|||
"optional": "省略可",
|
||||
"show_more": "もっと見る",
|
||||
"show_less": "たたむ",
|
||||
"cancel": "キャンセル"
|
||||
"cancel": "キャンセル",
|
||||
"disable": "無効",
|
||||
"enable": "有効",
|
||||
"confirm": "確認",
|
||||
"verify": "検査"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "画像を切り抜く",
|
||||
|
@ -48,7 +52,15 @@
|
|||
"placeholder": "例: lain",
|
||||
"register": "登録",
|
||||
"username": "ユーザー名",
|
||||
"hint": "会話に加わるには、ログインしてください"
|
||||
"hint": "会話に加わるには、ログインしてください",
|
||||
"authentication_code": "認証コード",
|
||||
"enter_recovery_code": "リカバリーコードを入力してください",
|
||||
"enter_two_factor_code": "2段階認証コードを入力してください",
|
||||
"recovery_code": "リカバリーコード",
|
||||
"heading" : {
|
||||
"totp" : "2段階認証",
|
||||
"recovery" : "2段階リカバリー"
|
||||
}
|
||||
},
|
||||
"media_modal": {
|
||||
"previous": "前",
|
||||
|
@ -79,6 +91,20 @@
|
|||
"repeated_you": "あなたのステータスがリピートされました",
|
||||
"no_more_notifications": "通知はありません"
|
||||
},
|
||||
"polls": {
|
||||
"add_poll": "投票を追加",
|
||||
"add_option": "選択肢を追加",
|
||||
"option": "選択肢",
|
||||
"votes": "票",
|
||||
"vote": "投票",
|
||||
"type": "投票の形式",
|
||||
"single_choice": "択一式",
|
||||
"multiple_choices": "複数選択式",
|
||||
"expiry": "投票期間",
|
||||
"expires_in": "投票は {0} で終了します",
|
||||
"expired": "投票は {0} 前に終了しました",
|
||||
"not_enough_options": "相異なる選択肢が不足しています"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "リピートとお気に入り",
|
||||
"follows": "新しいフォロワー",
|
||||
|
@ -139,6 +165,29 @@
|
|||
},
|
||||
"settings": {
|
||||
"app_name": "アプリの名称",
|
||||
"security": "セキュリティ",
|
||||
"enter_current_password_to_confirm": "あなたのアイデンティティを証明するため、現在のパスワードを入力してください",
|
||||
"mfa": {
|
||||
"otp" : "OTP",
|
||||
"setup_otp" : "OTPのセットアップ",
|
||||
"wait_pre_setup_otp" : "OTPのプリセット",
|
||||
"confirm_and_enable" : "OTPの確認と有効化",
|
||||
"title": "2段階認証",
|
||||
"generate_new_recovery_codes" : "新しいリカバリーコードを生成",
|
||||
"warning_of_generate_new_codes" : "新しいリカバリーコードを生成すると、古いコードは使用できなくなります。",
|
||||
"recovery_codes" : "リカバリーコード。",
|
||||
"waiting_a_recovery_codes": "バックアップコードを受信しています...",
|
||||
"recovery_codes_warning" : "コードを紙に書くか、安全な場所に保存してください。そうでなければ、あなたはコードを再び見ることはできません。もし2段階認証アプリのアクセスを喪失し、なおかつ、リカバリーコードもないならば、あなたは自分のアカウントから閉め出されます。",
|
||||
"authentication_methods" : "認証方法",
|
||||
"scan": {
|
||||
"title": "スキャン",
|
||||
"desc": "あなたの2段階認証アプリを使って、このQRコードをスキャンするか、テキストキーを入力してください:",
|
||||
"secret_code": "キー"
|
||||
},
|
||||
"verify": {
|
||||
"desc": "2段階認証を有効にするには、あなたの2段階認証アプリのコードを入力してください:"
|
||||
}
|
||||
},
|
||||
"attachmentRadius": "ファイル",
|
||||
"attachments": "ファイル",
|
||||
"autoload": "下にスクロールしたとき、自動的に読み込む。",
|
||||
|
|
|
@ -38,7 +38,8 @@
|
|||
"interactions": "Взаимодействия",
|
||||
"public_tl": "Публичная лента",
|
||||
"timeline": "Лента",
|
||||
"twkn": "Федеративная лента"
|
||||
"twkn": "Федеративная лента",
|
||||
"search": "Поиск"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "Неизвестный статус, ищем...",
|
||||
|
@ -381,5 +382,12 @@
|
|||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "Лента пользователя"
|
||||
},
|
||||
"search": {
|
||||
"people": "Люди",
|
||||
"hashtags": "Хэштэги",
|
||||
"person_talking": "Популярно у {count} человека",
|
||||
"people_talking": "Популярно у {count} человек",
|
||||
"no_results": "Ничего не найдено"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import messages from './i18n/messages.js'
|
|||
import VueChatScroll from 'vue-chat-scroll'
|
||||
import VueClickOutside from 'v-click-outside'
|
||||
import PortalVue from 'portal-vue'
|
||||
import VTooltip from 'v-tooltip'
|
||||
|
||||
import afterStoreSetup from './boot/after_store.js'
|
||||
|
||||
|
@ -37,6 +38,7 @@ Vue.use(VueI18n)
|
|||
Vue.use(VueChatScroll)
|
||||
Vue.use(VueClickOutside)
|
||||
Vue.use(PortalVue)
|
||||
Vue.use(VTooltip)
|
||||
|
||||
const i18n = new VueI18n({
|
||||
// By default, use the browser locale, we will update it if neccessary
|
||||
|
|
|
@ -492,10 +492,19 @@ export const mutations = {
|
|||
queueFlush (state, { timeline, id }) {
|
||||
state.timelines[timeline].flushMarker = id
|
||||
},
|
||||
addFavsAndRepeats (state, { id, favoritedByUsers, rebloggedByUsers }) {
|
||||
addRepeats (state, { id, rebloggedByUsers, currentUser }) {
|
||||
const newStatus = state.allStatusesObject[id]
|
||||
newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
|
||||
// repeats stats can be incorrect based on polling condition, let's update them using the most recent data
|
||||
newStatus.repeat_num = newStatus.rebloggedBy.length
|
||||
newStatus.repeated = !!newStatus.rebloggedBy.find(({ id }) => currentUser.id === id)
|
||||
},
|
||||
addFavs (state, { id, favoritedByUsers, currentUser }) {
|
||||
const newStatus = state.allStatusesObject[id]
|
||||
newStatus.favoritedBy = favoritedByUsers.filter(_ => _)
|
||||
newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
|
||||
// favorites stats can be incorrect based on polling condition, let's update them using the most recent data
|
||||
newStatus.fave_num = newStatus.favoritedBy.length
|
||||
newStatus.favorited = !!newStatus.favoritedBy.find(({ id }) => currentUser.id === id)
|
||||
},
|
||||
updateStatusWithPoll (state, { id, poll }) {
|
||||
const status = state.allStatusesObject[id]
|
||||
|
@ -581,9 +590,26 @@ const statuses = {
|
|||
Promise.all([
|
||||
rootState.api.backendInteractor.fetchFavoritedByUsers(id),
|
||||
rootState.api.backendInteractor.fetchRebloggedByUsers(id)
|
||||
]).then(([favoritedByUsers, rebloggedByUsers]) =>
|
||||
commit('addFavsAndRepeats', { id, favoritedByUsers, rebloggedByUsers })
|
||||
)
|
||||
]).then(([favoritedByUsers, rebloggedByUsers]) => {
|
||||
commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser })
|
||||
commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })
|
||||
})
|
||||
},
|
||||
fetchFavs ({ rootState, commit }, id) {
|
||||
rootState.api.backendInteractor.fetchFavoritedByUsers(id)
|
||||
.then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }))
|
||||
},
|
||||
fetchRepeats ({ rootState, commit }, id) {
|
||||
rootState.api.backendInteractor.fetchRebloggedByUsers(id)
|
||||
.then(rebloggedByUsers => commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser }))
|
||||
},
|
||||
search (store, { q, resolve, limit, offset, following }) {
|
||||
return store.rootState.api.backendInteractor.search2({ q, resolve, limit, offset, following })
|
||||
.then((data) => {
|
||||
store.commit('addNewUsers', data.accounts)
|
||||
store.commit('addNewStatuses', { statuses: data.statuses })
|
||||
return data
|
||||
})
|
||||
}
|
||||
},
|
||||
mutations
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import userSearchApi from '../services/new_api/user_search.js'
|
||||
import oauthApi from '../services/new_api/oauth.js'
|
||||
import { compact, map, each, merge, last, concat, uniq } from 'lodash'
|
||||
import { set } from 'vue'
|
||||
|
@ -136,6 +135,7 @@ export const mutations = {
|
|||
user.following = relationship.following
|
||||
user.muted = relationship.muting
|
||||
user.statusnet_blocking = relationship.blocking
|
||||
user.subscribed = relationship.subscribing
|
||||
}
|
||||
})
|
||||
},
|
||||
|
@ -305,6 +305,14 @@ const users = {
|
|||
clearFollowers ({ commit }, userId) {
|
||||
commit('clearFollowers', userId)
|
||||
},
|
||||
subscribeUser ({ rootState, commit }, id) {
|
||||
return rootState.api.backendInteractor.subscribeUser(id)
|
||||
.then((relationship) => commit('updateUserRelationship', [relationship]))
|
||||
},
|
||||
unsubscribeUser ({ rootState, commit }, id) {
|
||||
return rootState.api.backendInteractor.unsubscribeUser(id)
|
||||
.then((relationship) => commit('updateUserRelationship', [relationship]))
|
||||
},
|
||||
registerPushNotifications (store) {
|
||||
const token = store.state.currentUser.credentials
|
||||
const vapidPublicKey = store.rootState.instance.vapidPublicKey
|
||||
|
@ -356,14 +364,7 @@ const users = {
|
|||
})
|
||||
},
|
||||
searchUsers (store, query) {
|
||||
// TODO: Move userSearch api into api.service
|
||||
return userSearchApi.search({
|
||||
query,
|
||||
store: {
|
||||
state: store.rootState,
|
||||
getters: store.rootGetters
|
||||
}
|
||||
})
|
||||
return store.rootState.api.backendInteractor.searchUsers(query)
|
||||
.then((users) => {
|
||||
store.commit('addNewUsers', users)
|
||||
return users
|
||||
|
|
|
@ -55,6 +55,8 @@ const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
|
|||
const MASTODON_UNBLOCK_USER_URL = id => `/api/v1/accounts/${id}/unblock`
|
||||
const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
|
||||
const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
|
||||
const MASTODON_SUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/subscribe`
|
||||
const MASTODON_UNSUBSCRIBE_USER = id => `/api/v1/pleroma/accounts/${id}/unsubscribe`
|
||||
const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
|
||||
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
|
||||
const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes`
|
||||
|
@ -65,6 +67,8 @@ const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials'
|
|||
const MASTODON_REPORT_USER_URL = '/api/v1/reports'
|
||||
const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin`
|
||||
const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin`
|
||||
const MASTODON_SEARCH_2 = `/api/v2/search`
|
||||
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
|
||||
|
||||
const oldfetch = window.fetch
|
||||
|
||||
|
@ -76,7 +80,7 @@ let fetch = (url, options) => {
|
|||
return oldfetch(fullUrl, options)
|
||||
}
|
||||
|
||||
const promisedRequest = ({ method, url, payload, credentials, headers = {} }) => {
|
||||
const promisedRequest = ({ method, url, params, payload, credentials, headers = {} }) => {
|
||||
const options = {
|
||||
method,
|
||||
headers: {
|
||||
|
@ -85,6 +89,11 @@ const promisedRequest = ({ method, url, payload, credentials, headers = {} }) =>
|
|||
...headers
|
||||
}
|
||||
}
|
||||
if (params) {
|
||||
url += '?' + Object.entries(params)
|
||||
.map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value))
|
||||
.join('&')
|
||||
}
|
||||
if (payload) {
|
||||
options.body = JSON.stringify(payload)
|
||||
}
|
||||
|
@ -746,6 +755,14 @@ const unmuteUser = ({ id, credentials }) => {
|
|||
return promisedRequest({ url: MASTODON_UNMUTE_USER_URL(id), credentials, method: 'POST' })
|
||||
}
|
||||
|
||||
const subscribeUser = ({ id, credentials }) => {
|
||||
return promisedRequest({ url: MASTODON_SUBSCRIBE_USER(id), credentials, method: 'POST' })
|
||||
}
|
||||
|
||||
const unsubscribeUser = ({ id, credentials }) => {
|
||||
return promisedRequest({ url: MASTODON_UNSUBSCRIBE_USER(id), credentials, method: 'POST' })
|
||||
}
|
||||
|
||||
const fetchBlocks = ({ credentials }) => {
|
||||
return promisedRequest({ url: MASTODON_USER_BLOCKS_URL, credentials })
|
||||
.then((users) => users.map(parseUser))
|
||||
|
@ -837,6 +854,60 @@ const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const searchUsers = ({ credentials, query }) => {
|
||||
return promisedRequest({
|
||||
url: MASTODON_USER_SEARCH_URL,
|
||||
params: {
|
||||
q: query,
|
||||
resolve: true
|
||||
},
|
||||
credentials
|
||||
})
|
||||
.then((data) => data.map(parseUser))
|
||||
}
|
||||
|
||||
const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
|
||||
let url = MASTODON_SEARCH_2
|
||||
let params = []
|
||||
|
||||
if (q) {
|
||||
params.push(['q', encodeURIComponent(q)])
|
||||
}
|
||||
|
||||
if (resolve) {
|
||||
params.push(['resolve', resolve])
|
||||
}
|
||||
|
||||
if (limit) {
|
||||
params.push(['limit', limit])
|
||||
}
|
||||
|
||||
if (offset) {
|
||||
params.push(['offset', offset])
|
||||
}
|
||||
|
||||
if (following) {
|
||||
params.push(['following', true])
|
||||
}
|
||||
|
||||
let queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
|
||||
url += `?${queryString}`
|
||||
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((data) => {
|
||||
if (data.ok) {
|
||||
return data
|
||||
}
|
||||
throw new Error('Error fetching search result', data)
|
||||
})
|
||||
.then((data) => { return data.json() })
|
||||
.then((data) => {
|
||||
data.accounts = data.accounts.slice(0, limit).map(u => parseUser(u))
|
||||
data.statuses = data.statuses.slice(0, limit).map(s => parseStatus(s))
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
const apiService = {
|
||||
verifyCredentials,
|
||||
fetchTimeline,
|
||||
|
@ -864,6 +935,8 @@ const apiService = {
|
|||
fetchMutes,
|
||||
muteUser,
|
||||
unmuteUser,
|
||||
subscribeUser,
|
||||
unsubscribeUser,
|
||||
fetchBlocks,
|
||||
fetchOAuthTokens,
|
||||
revokeOAuthToken,
|
||||
|
@ -899,7 +972,9 @@ const apiService = {
|
|||
fetchFavoritedByUsers,
|
||||
fetchRebloggedByUsers,
|
||||
reportUser,
|
||||
updateNotificationSettings
|
||||
updateNotificationSettings,
|
||||
search2,
|
||||
searchUsers
|
||||
}
|
||||
|
||||
export default apiService
|
||||
|
|
|
@ -108,6 +108,8 @@ const backendInteractorService = credentials => {
|
|||
const fetchMutes = () => apiService.fetchMutes({ credentials })
|
||||
const muteUser = (id) => apiService.muteUser({ credentials, id })
|
||||
const unmuteUser = (id) => apiService.unmuteUser({ credentials, id })
|
||||
const subscribeUser = (id) => apiService.subscribeUser({ credentials, id })
|
||||
const unsubscribeUser = (id) => apiService.unsubscribeUser({ credentials, id })
|
||||
const fetchBlocks = () => apiService.fetchBlocks({ credentials })
|
||||
const fetchFollowRequests = () => apiService.fetchFollowRequests({ credentials })
|
||||
const fetchOAuthTokens = () => apiService.fetchOAuthTokens({ credentials })
|
||||
|
@ -146,6 +148,9 @@ const backendInteractorService = credentials => {
|
|||
const unfavorite = (id) => apiService.unfavorite({ id, credentials })
|
||||
const retweet = (id) => apiService.retweet({ id, credentials })
|
||||
const unretweet = (id) => apiService.unretweet({ id, credentials })
|
||||
const search2 = ({ q, resolve, limit, offset, following }) =>
|
||||
apiService.search2({ credentials, q, resolve, limit, offset, following })
|
||||
const searchUsers = (query) => apiService.searchUsers({ query, credentials })
|
||||
|
||||
const backendInteractorServiceInstance = {
|
||||
fetchStatus,
|
||||
|
@ -165,6 +170,8 @@ const backendInteractorService = credentials => {
|
|||
fetchMutes,
|
||||
muteUser,
|
||||
unmuteUser,
|
||||
subscribeUser,
|
||||
unsubscribeUser,
|
||||
fetchBlocks,
|
||||
fetchOAuthTokens,
|
||||
revokeOAuthToken,
|
||||
|
@ -205,7 +212,9 @@ const backendInteractorService = credentials => {
|
|||
unfavorite,
|
||||
retweet,
|
||||
unretweet,
|
||||
updateNotificationSettings
|
||||
updateNotificationSettings,
|
||||
search2,
|
||||
searchUsers
|
||||
}
|
||||
|
||||
return backendInteractorServiceInstance
|
||||
|
|
|
@ -68,6 +68,7 @@ export const parseUser = (data) => {
|
|||
output.following = relationship.following
|
||||
output.statusnet_blocking = relationship.blocking
|
||||
output.muted = relationship.muting
|
||||
output.subscribed = relationship.subscribing
|
||||
}
|
||||
|
||||
output.hide_follows = data.pleroma.hide_follows
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import utils from './utils.js'
|
||||
import { parseUser } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
|
||||
const search = ({ query, store }) => {
|
||||
return utils.request({
|
||||
store,
|
||||
url: '/api/v1/accounts/search',
|
||||
params: {
|
||||
q: query,
|
||||
resolve: true
|
||||
}
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.then((data) => data.map(parseUser))
|
||||
}
|
||||
const UserSearch = {
|
||||
search
|
||||
}
|
||||
|
||||
export default UserSearch
|
|
@ -1,36 +0,0 @@
|
|||
const queryParams = (params) => {
|
||||
return Object.keys(params)
|
||||
.map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k]))
|
||||
.join('&')
|
||||
}
|
||||
|
||||
const headers = (store) => {
|
||||
const accessToken = store.getters.getToken()
|
||||
if (accessToken) {
|
||||
return { 'Authorization': `Bearer ${accessToken}` }
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const request = ({ method = 'GET', url, params, store }) => {
|
||||
const instance = store.state.instance.server
|
||||
let fullUrl = `${instance}${url}`
|
||||
|
||||
if (method === 'GET' && params) {
|
||||
fullUrl = fullUrl + `?${queryParams(params)}`
|
||||
}
|
||||
|
||||
return window.fetch(fullUrl, {
|
||||
method,
|
||||
headers: headers(store),
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
}
|
||||
|
||||
const utils = {
|
||||
queryParams,
|
||||
request
|
||||
}
|
||||
|
||||
export default utils
|
0
static/font/LICENSE.txt
Normal file → Executable file
0
static/font/LICENSE.txt
Normal file → Executable file
0
static/font/README.txt
Normal file → Executable file
0
static/font/README.txt
Normal file → Executable file
20
static/font/config.json
Normal file → Executable file
20
static/font/config.json
Normal file → Executable file
|
@ -150,12 +150,6 @@
|
|||
"code": 61669,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "cd21cbfb28ad4d903cede582157f65dc",
|
||||
"css": "bell",
|
||||
"code": 59408,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "ccc2329632396dc096bb638d4b46fb98",
|
||||
"css": "mail-alt",
|
||||
|
@ -277,6 +271,20 @@
|
|||
"search": [
|
||||
"ellipsis"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uid": "0bef873af785ead27781fdf98b3ae740",
|
||||
"css": "bell-ringing-o",
|
||||
"code": 59408,
|
||||
"src": "custom_icons",
|
||||
"selected": true,
|
||||
"svg": {
|
||||
"path": "M497.8 0C468.3 0 444.4 23.9 444.4 53.3 444.4 61.1 446.1 68.3 448.9 75 301.7 96.7 213.3 213.3 213.3 320 213.3 588.3 117.8 712.8 35.6 782.2 35.6 821.1 67.8 853.3 106.7 853.3H355.6C355.6 931.7 419.4 995.6 497.8 995.6S640 931.7 640 853.3H888.9C927.8 853.3 960 821.1 960 782.2 877.8 712.8 782.2 588.3 782.2 320 782.2 213.3 693.9 96.7 546.7 75 549.4 68.3 551.1 61.1 551.1 53.3 551.1 23.9 527.2 0 497.8 0ZM189.4 44.8C108.4 118.6 70.5 215.1 71.1 320.2L142.2 319.8C141.7 231.2 170.4 158.3 237.3 97.4L189.4 44.8ZM806.2 44.8L758.3 97.4C825.2 158.3 853.9 231.2 853.3 319.8L924.4 320.2C925.1 215.1 887.2 118.6 806.2 44.8ZM408.9 844.4C413.9 844.4 417.8 848.3 417.8 853.3 417.8 897.2 453.9 933.3 497.8 933.3 502.8 933.3 506.7 937.2 506.7 942.2S502.8 951.1 497.8 951.1C443.9 951.1 400 907.2 400 853.3 400 848.3 403.9 844.4 408.9 844.4Z",
|
||||
"width": 1000
|
||||
},
|
||||
"search": [
|
||||
"bell-ringing-o"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
2
static/font/css/fontello-codes.css
vendored
2
static/font/css/fontello-codes.css
vendored
|
@ -15,7 +15,7 @@
|
|||
.icon-right-open:before { content: '\e80d'; } /* '' */
|
||||
.icon-left-open:before { content: '\e80e'; } /* '' */
|
||||
.icon-up-open:before { content: '\e80f'; } /* '' */
|
||||
.icon-bell:before { content: '\e810'; } /* '' */
|
||||
.icon-bell-ringing-o:before { content: '\e810'; } /* '' */
|
||||
.icon-lock:before { content: '\e811'; } /* '' */
|
||||
.icon-globe:before { content: '\e812'; } /* '' */
|
||||
.icon-brush:before { content: '\e813'; } /* '' */
|
||||
|
|
14
static/font/css/fontello-embedded.css
vendored
14
static/font/css/fontello-embedded.css
vendored
File diff suppressed because one or more lines are too long
2
static/font/css/fontello-ie7-codes.css
vendored
2
static/font/css/fontello-ie7-codes.css
vendored
|
@ -15,7 +15,7 @@
|
|||
.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-bell-ringing-o { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
2
static/font/css/fontello-ie7.css
vendored
2
static/font/css/fontello-ie7.css
vendored
|
@ -26,7 +26,7 @@
|
|||
.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-bell-ringing-o { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
.icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||
|
|
16
static/font/css/fontello.css
vendored
16
static/font/css/fontello.css
vendored
|
@ -1,11 +1,11 @@
|
|||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('../font/fontello.eot?3304725');
|
||||
src: url('../font/fontello.eot?3304725#iefix') format('embedded-opentype'),
|
||||
url('../font/fontello.woff2?3304725') format('woff2'),
|
||||
url('../font/fontello.woff?3304725') format('woff'),
|
||||
url('../font/fontello.ttf?3304725') format('truetype'),
|
||||
url('../font/fontello.svg?3304725#fontello') format('svg');
|
||||
src: url('../font/fontello.eot?91349539');
|
||||
src: url('../font/fontello.eot?91349539#iefix') format('embedded-opentype'),
|
||||
url('../font/fontello.woff2?91349539') format('woff2'),
|
||||
url('../font/fontello.woff?91349539') format('woff'),
|
||||
url('../font/fontello.ttf?91349539') format('truetype'),
|
||||
url('../font/fontello.svg?91349539#fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -15,7 +15,7 @@
|
|||
@media screen and (-webkit-min-device-pixel-ratio:0) {
|
||||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('../font/fontello.svg?3304725#fontello') format('svg');
|
||||
src: url('../font/fontello.svg?91349539#fontello') format('svg');
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
@ -71,7 +71,7 @@
|
|||
.icon-right-open:before { content: '\e80d'; } /* '' */
|
||||
.icon-left-open:before { content: '\e80e'; } /* '' */
|
||||
.icon-up-open:before { content: '\e80f'; } /* '' */
|
||||
.icon-bell:before { content: '\e810'; } /* '' */
|
||||
.icon-bell-ringing-o:before { content: '\e810'; } /* '' */
|
||||
.icon-lock:before { content: '\e811'; } /* '' */
|
||||
.icon-globe:before { content: '\e812'; } /* '' */
|
||||
.icon-brush:before { content: '\e813'; } /* '' */
|
||||
|
|
12
static/font/demo.html
Normal file → Executable file
12
static/font/demo.html
Normal file → Executable file
|
@ -229,11 +229,11 @@ body {
|
|||
}
|
||||
@font-face {
|
||||
font-family: 'fontello';
|
||||
src: url('./font/fontello.eot?14310629');
|
||||
src: url('./font/fontello.eot?14310629#iefix') format('embedded-opentype'),
|
||||
url('./font/fontello.woff?14310629') format('woff'),
|
||||
url('./font/fontello.ttf?14310629') format('truetype'),
|
||||
url('./font/fontello.svg?14310629#fontello') format('svg');
|
||||
src: url('./font/fontello.eot?82370835');
|
||||
src: url('./font/fontello.eot?82370835#iefix') format('embedded-opentype'),
|
||||
url('./font/fontello.woff?82370835') format('woff'),
|
||||
url('./font/fontello.ttf?82370835') format('truetype'),
|
||||
url('./font/fontello.svg?82370835#fontello') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
@ -322,7 +322,7 @@ body {
|
|||
<div class="the-icons span3" title="Code: 0xe80f"><i class="demo-icon icon-up-open"></i> <span class="i-name">icon-up-open</span><span class="i-code">0xe80f</span></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="the-icons span3" title="Code: 0xe810"><i class="demo-icon icon-bell"></i> <span class="i-name">icon-bell</span><span class="i-code">0xe810</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe810"><i class="demo-icon icon-bell-ringing-o"></i> <span class="i-name">icon-bell-ringing-o</span><span class="i-code">0xe810</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe811"><i class="demo-icon icon-lock"></i> <span class="i-name">icon-lock</span><span class="i-code">0xe811</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe812"><i class="demo-icon icon-globe"></i> <span class="i-name">icon-globe</span><span class="i-code">0xe812</span></div>
|
||||
<div class="the-icons span3" title="Code: 0xe813"><i class="demo-icon icon-brush"></i> <span class="i-name">icon-brush</span><span class="i-code">0xe813</span></div>
|
||||
|
|
Binary file not shown.
|
@ -38,7 +38,7 @@
|
|||
|
||||
<glyph glyph-name="up-open" unicode="" d="M939 114l-92-92q-11-10-26-10t-25 10l-296 297-296-297q-11-10-25-10t-25 10l-93 92q-11 11-11 26t11 25l414 414q11 10 25 10t25-10l414-414q11-11 11-25t-11-26z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="bell" unicode="" d="M509-89q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m-372 160h726q-149 168-149 465 0 28-13 58t-39 58-67 45-95 17-95-17-67-45-39-58-13-58q0-297-149-465z m827 0q0-29-21-50t-50-21h-250q0-59-42-101t-101-42-101 42-42 101h-250q-29 0-50 21t-21 50q28 24 51 49t47 67 42 89 27 115 11 145q0 84 66 157t171 89q-5 10-5 21 0 23 16 38t38 16 38-16 16-38q0-11-5-21 106-16 171-89t66-157q0-78 11-145t28-115 41-89 48-67 50-49z" horiz-adv-x="1000" />
|
||||
<glyph glyph-name="bell-ringing-o" unicode="" d="M498 857c-30 0-54-24-54-53 0-8 2-15 5-22-147-22-236-138-236-245 0-268-95-393-177-462 0-39 32-71 71-71h249c0-79 63-143 142-143s142 64 142 143h249c39 0 71 32 71 71-82 69-178 194-178 462 0 107-88 223-235 245 2 7 4 14 4 22 0 29-24 53-53 53z m-309-45c-81-74-118-170-118-275l71 0c0 89 28 162 95 223l-48 52z m617 0l-48-52c67-61 96-134 95-223l71 0c1 105-37 201-118 275z m-397-799c5 0 9-4 9-9 0-44 36-80 80-80 5 0 9-4 9-9s-4-9-9-9c-54 0-98 44-98 98 0 5 4 9 9 9z" horiz-adv-x="1000" />
|
||||
|
||||
<glyph glyph-name="lock" unicode="" d="M179 428h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" />
|
||||
|
||||
|
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
25
yarn.lock
25
yarn.lock
|
@ -5459,9 +5459,10 @@ pngjs@^3.3.0:
|
|||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b"
|
||||
|
||||
popper.js@^1.14.7:
|
||||
version "1.14.7"
|
||||
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e"
|
||||
popper.js@^1.15.0:
|
||||
version "1.15.0"
|
||||
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"
|
||||
integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA==
|
||||
|
||||
portal-vue@^2.1.4:
|
||||
version "2.1.4"
|
||||
|
@ -7198,6 +7199,15 @@ v-click-outside@^2.1.1:
|
|||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-2.1.3.tgz#b7297abe833a439dc0895e6418a494381e64b5e7"
|
||||
|
||||
v-tooltip@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/v-tooltip/-/v-tooltip-2.0.2.tgz#8610d9eece2cc44fd66c12ef2f12eec6435cab9b"
|
||||
integrity sha512-xQ+qzOFfywkLdjHknRPgMMupQNS8yJtf9Utd5Dxiu/0n4HtrxqsgDtN2MLZ0LKbburtSAQgyypuE/snM8bBZhw==
|
||||
dependencies:
|
||||
lodash "^4.17.11"
|
||||
popper.js "^1.15.0"
|
||||
vue-resize "^0.4.5"
|
||||
|
||||
validate-npm-package-license@^3.0.1:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
|
||||
|
@ -7272,11 +7282,10 @@ vue-loader@^14.0.0:
|
|||
vue-style-loader "^4.0.1"
|
||||
vue-template-es2015-compiler "^1.6.0"
|
||||
|
||||
vue-popperjs@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-popperjs/-/vue-popperjs-2.0.3.tgz#7c446d0ba7c63170ccb33a02669d0df4efc3d8cd"
|
||||
dependencies:
|
||||
popper.js "^1.14.7"
|
||||
vue-resize@^0.4.5:
|
||||
version "0.4.5"
|
||||
resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-0.4.5.tgz#4777a23042e3c05620d9cbda01c0b3cc5e32dcea"
|
||||
integrity sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg==
|
||||
|
||||
vue-router@^3.0.1:
|
||||
version "3.0.2"
|
||||
|
|
Loading…
Add table
Reference in a new issue