Merge branch 'develop' into 'feat/conversation-muting'

# Conflicts:
#   src/services/api/api.service.js
This commit is contained in:
Shpuld Shpludson 2019-07-15 19:09:01 +00:00
commit 3370dd80dc
45 changed files with 717 additions and 387 deletions

View File

@ -24,6 +24,9 @@ var devMiddleware = require('webpack-dev-middleware')(compiler, {
stats: { stats: {
colors: true, colors: true,
chunks: false chunks: false
},
headers: {
'content-security-policy': "base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; script-src 'self' 'unsafe-eval';"
} }
}) })

View File

@ -1,7 +1,7 @@
import UserPanel from './components/user_panel/user_panel.vue' import UserPanel from './components/user_panel/user_panel.vue'
import NavPanel from './components/nav_panel/nav_panel.vue' import NavPanel from './components/nav_panel/nav_panel.vue'
import Notifications from './components/notifications/notifications.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 InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
import FeaturesPanel from './components/features_panel/features_panel.vue' import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
@ -19,7 +19,7 @@ export default {
UserPanel, UserPanel,
NavPanel, NavPanel,
Notifications, Notifications,
UserFinder, SearchBar,
InstanceSpecificPanel, InstanceSpecificPanel,
FeaturesPanel, FeaturesPanel,
WhoToFollowPanel, WhoToFollowPanel,
@ -32,7 +32,7 @@ export default {
}, },
data: () => ({ data: () => ({
mobileActivePanel: 'timeline', mobileActivePanel: 'timeline',
finderHidden: true, searchBarHidden: true,
supportsMask: window.CSS && window.CSS.supports && ( supportsMask: window.CSS && window.CSS.supports && (
window.CSS.supports('mask-size', 'contain') || window.CSS.supports('mask-size', 'contain') ||
window.CSS.supports('-webkit-mask-size', 'contain') || window.CSS.supports('-webkit-mask-size', 'contain') ||
@ -70,7 +70,7 @@ export default {
logoBgStyle () { logoBgStyle () {
return Object.assign({ return Object.assign({
'margin': `${this.$store.state.instance.logoMargin} 0`, 'margin': `${this.$store.state.instance.logoMargin} 0`,
opacity: this.finderHidden ? 1 : 0 opacity: this.searchBarHidden ? 1 : 0
}, this.enableMask ? {} : { }, this.enableMask ? {} : {
'background-color': this.enableMask ? '' : 'transparent' 'background-color': this.enableMask ? '' : 'transparent'
}) })
@ -101,8 +101,8 @@ export default {
this.$router.replace('/main/public') this.$router.replace('/main/public')
this.$store.dispatch('logout') this.$store.dispatch('logout')
}, },
onFinderToggled (hidden) { onSearchBarToggled (hidden) {
this.finderHidden = hidden this.searchBarHidden = hidden
}, },
updateMobileState () { updateMobileState () {
const mobileLayout = windowWidth() <= 800 const mobileLayout = windowWidth() <= 800

View File

@ -283,6 +283,31 @@ i[class*=icon-] {
color: var(--icon, $fallback--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 { .container {
display: flex; display: flex;

View File

@ -38,9 +38,9 @@
</router-link> </router-link>
</div> </div>
<div class="item right"> <div class="item right">
<user-finder <search-bar
class="button-icon nav-icon mobile-hidden" class="nav-icon mobile-hidden"
@toggled="onFinderToggled" @toggled="onSearchBarToggled"
/> />
<router-link <router-link
class="mobile-hidden" class="mobile-hidden"

View File

@ -6,12 +6,12 @@ import ConversationPage from 'components/conversation-page/conversation-page.vue
import Interactions from 'components/interactions/interactions.vue' import Interactions from 'components/interactions/interactions.vue'
import DMs from 'components/dm_timeline/dm_timeline.vue' import DMs from 'components/dm_timeline/dm_timeline.vue'
import UserProfile from 'components/user_profile/user_profile.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 Settings from 'components/settings/settings.vue'
import Registration from 'components/registration/registration.vue' import Registration from 'components/registration/registration.vue'
import UserSettings from 'components/user_settings/user_settings.vue' import UserSettings from 'components/user_settings/user_settings.vue'
import FollowRequests from 'components/follow_requests/follow_requests.vue' import FollowRequests from 'components/follow_requests/follow_requests.vue'
import OAuthCallback from 'components/oauth_callback/oauth_callback.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 Notifications from 'components/notifications/notifications.vue'
import AuthForm from 'components/auth_form/auth_form.js' import AuthForm from 'components/auth_form/auth_form.js'
import ChatPanel from 'components/chat_panel/chat_panel.vue' import ChatPanel from 'components/chat_panel/chat_panel.vue'
@ -45,7 +45,7 @@ export default (store) => {
{ name: 'login', path: '/login', component: AuthForm }, { name: 'login', path: '/login', component: AuthForm },
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) }, { name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, { 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: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow }, { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow },
{ name: 'about', path: '/about', component: About }, { name: 'about', path: '/about', component: About },
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile } { name: 'user-profile', path: '/(users/)?:name', component: UserProfile }

View File

@ -100,7 +100,7 @@
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1> <h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1>
<div v-html="attachment.oembed.oembedHTML" /> <div v-html="attachment.oembed.oembedHTML" />
<!-- eslint-enabled vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,8 +1,5 @@
<template> <template>
<div <div>
class="block"
style="position: relative"
>
<Popper <Popper
trigger="click" trigger="click"
append-to-body append-to-body
@ -131,6 +128,7 @@
</div> </div>
<button <button
slot="reference" slot="reference"
class="btn btn-default btn-block"
:class="{ pressed: showDropDown }" :class="{ pressed: showDropDown }"
@click="toggleMenu" @click="toggleMenu"
> >

View File

@ -3,7 +3,7 @@
:disabled="progress || disabled" :disabled="progress || disabled"
@click="onClick" @click="onClick"
> >
<template v-if="progress"> <template v-if="progress && $slots.progress">
<slot name="progress" /> <slot name="progress" />
</template> </template>
<template v-else> <template v-else>

View 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

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

View 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

View File

@ -1,36 +1,36 @@
<template> <template>
<div> <div>
<div class="user-finder-container"> <div class="search-bar-container">
<i <i
v-if="loading" v-if="loading"
class="icon-spin4 user-finder-icon animate-spin-slow" class="icon-spin4 finder-icon animate-spin-slow"
/> />
<a <a
v-if="hidden" v-if="hidden"
href="#" href="#"
:title="$t('finder.find_user')" :title="$t('nav.search')"
><i ><i
class="icon-user-plus user-finder-icon" class="button-icon icon-search"
@click.prevent.stop="toggleHidden" @click.prevent.stop="toggleHidden"
/></a> /></a>
<template v-else> <template v-else>
<input <input
id="user-finder-input" id="search-bar-input"
ref="userSearchInput" ref="searchInput"
v-model="username" v-model="searchTerm"
class="user-finder-input" class="search-bar-input"
:placeholder="$t('finder.find_user')" :placeholder="$t('nav.search')"
type="text" type="text"
@keyup.enter="findUser(username)" @keyup.enter="find(searchTerm)"
> >
<button <button
class="btn search-button" class="btn search-button"
@click="findUser(username)" @click="find(searchTerm)"
> >
<i class="icon-search" /> <i class="icon-search" />
</button> </button>
<i <i
class="button-icon icon-cancel user-finder-icon" class="button-icon icon-cancel"
@click.prevent.stop="toggleHidden" @click.prevent.stop="toggleHidden"
/> />
</template> </template>
@ -38,22 +38,24 @@
</div> </div>
</template> </template>
<script src="./user_finder.js"></script> <script src="./search_bar.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.user-finder-container { .search-bar-container {
max-width: 100%; max-width: 100%;
display: inline-flex; display: inline-flex;
align-items: baseline; align-items: baseline;
vertical-align: baseline; vertical-align: baseline;
justify-content: flex-end;
.user-finder-input, .search-bar-input,
.search-button { .search-button {
height: 29px; height: 29px;
} }
.user-finder-input {
.search-bar-input {
// TODO: do this properly without a rough guesstimate of 2 icons + paddings // TODO: do this properly without a rough guesstimate of 2 icons + paddings
max-width: calc(100% - 30px - 30px - 20px); max-width: calc(100% - 30px - 30px - 20px);
} }
@ -62,6 +64,10 @@
margin-left: .5em; margin-left: .5em;
margin-right: .5em; margin-right: .5em;
} }
.icon-cancel {
cursor: pointer;
}
} }
</style> </style>

View File

@ -100,8 +100,8 @@
</ul> </ul>
<ul> <ul>
<li @click="toggleDrawer"> <li @click="toggleDrawer">
<router-link :to="{ name: 'user-search' }"> <router-link :to="{ name: 'search' }">
{{ $t("nav.user_search") }} {{ $t("nav.search") }}
</router-link> </router-link>
</li> </li>
<li <li

View File

@ -173,12 +173,13 @@ const Status = {
if (this.status.type === 'retweet') { if (this.status.type === 'retweet') {
return false return false
} }
var checkFollowing = this.$store.state.config.replyVisibility === 'following' const checkFollowing = this.$store.state.config.replyVisibility === 'following'
for (var i = 0; i < this.status.attentions.length; ++i) { for (var i = 0; i < this.status.attentions.length; ++i) {
if (this.status.user.id === this.status.attentions[i].id) { if (this.status.user.id === this.status.attentions[i].id) {
continue continue
} }
if (checkFollowing && this.$store.getters.findUser(this.status.attentions[i].id).following) { const taggedUser = this.$store.getters.findUser(this.status.attentions[i].id)
if (checkFollowing && taggedUser && taggedUser.following) {
return false return false
} }
if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) { if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) {
@ -418,6 +419,18 @@ const Status = {
window.scrollBy(0, rect.bottom - window.innerHeight + 50) 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: { filters: {

View File

@ -4,7 +4,7 @@ import './tab_switcher.scss'
export default Vue.component('tab-switcher', { export default Vue.component('tab-switcher', {
name: 'TabSwitcher', name: 'TabSwitcher',
props: ['renderOnlyFocused', 'onSwitch'], props: ['renderOnlyFocused', 'onSwitch', 'customActive'],
data () { data () {
return { return {
active: this.$slots.default.findIndex(_ => _.tag) active: this.$slots.default.findIndex(_ => _.tag)
@ -24,6 +24,14 @@ export default Vue.component('tab-switcher', {
} }
this.active = index 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) { render (h) {
@ -33,7 +41,7 @@ export default Vue.component('tab-switcher', {
const classesTab = ['tab'] const classesTab = ['tab']
const classesWrapper = ['tab-wrapper'] const classesWrapper = ['tab-wrapper']
if (index === this.active) { if (this.isActiveTab(index)) {
classesTab.push('active') classesTab.push('active')
classesWrapper.push('active') classesWrapper.push('active')
} }

View File

@ -1,5 +1,6 @@
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue' import RemoteFollow from '../remote_follow/remote_follow.vue'
import ProgressButton from '../progress_button/progress_button.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue' import ModerationTools from '../moderation_tools/moderation_tools.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js' import { hex2rgb } from '../../services/color_convert/color_convert.js'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
@ -104,7 +105,8 @@ export default {
components: { components: {
UserAvatar, UserAvatar,
RemoteFollow, RemoteFollow,
ModerationTools ModerationTools,
ProgressButton
}, },
methods: { methods: {
followUser () { followUser () {
@ -135,6 +137,12 @@ export default {
unmuteUser () { unmuteUser () {
this.$store.dispatch('unmuteUser', this.user.id) 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) { setProfileView (v) {
if (this.switcher) { if (this.switcher) {
const store = this.$store const store = this.$store

View File

@ -112,101 +112,120 @@
</div> </div>
</div> </div>
<div <div
v-if="isOtherUser" v-if="loggedIn && isOtherUser"
class="user-interactions" class="user-interactions"
> >
<div <div v-if="!user.following">
v-if="loggedIn" <button
class="follow" class="btn btn-default btn-block"
> :disabled="followRequestInProgress"
<span v-if="user.following"> :title="followRequestSent ? $t('user_card.follow_again') : ''"
<!--Following them!--> @click="followUser"
<button >
class="pressed" <template v-if="followRequestInProgress">
:disabled="followRequestInProgress" {{ $t('user_card.follow_progress') }}
:title="$t('user_card.follow_unfollow')" </template>
@click="unfollowUser" <template v-else-if="followRequestSent">
> {{ $t('user_card.follow_sent') }}
<template v-if="followRequestInProgress"> </template>
{{ $t('user_card.follow_progress') }} <template v-else>
</template> {{ $t('user_card.follow') }}
<template v-else> </template>
{{ $t('user_card.following') }} </button>
</template> </div>
</button> <div v-else-if="followRequestInProgress">
</span> <button
<span v-if="!user.following"> class="btn btn-default btn-block pressed"
<button disabled
:disabled="followRequestInProgress" :title="$t('user_card.follow_unfollow')"
:title="followRequestSent ? $t('user_card.follow_again') : ''" @click="unfollowUser"
@click="followUser" >
> {{ $t('user_card.follow_progress') }}
<template v-if="followRequestInProgress"> </button>
{{ $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> </div>
<div <div
v-if="isOtherUser && loggedIn" v-else
class="mute" class="btn-group"
> >
<span v-if="user.muted"> <button
<button class="btn btn-default pressed"
class="pressed" :title="$t('user_card.follow_unfollow')"
@click="unmuteUser" @click="unfollowUser"
> >
{{ $t('user_card.muted') }} {{ $t('user_card.following') }}
</button> </button>
</span> <ProgressButton
<span v-if="!user.muted"> v-if="!user.subscribed"
<button @click="muteUser"> class="btn btn-default"
{{ $t('user_card.mute') }} :click="subscribeUser"
</button> :title="$t('user_card.subscribe')"
</span> >
<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>
<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>
<div
v-if="isOtherUser && loggedIn" <div>
class="block" <button
> v-if="user.statusnet_blocking"
<span v-if="user.statusnet_blocking"> class="btn btn-default btn-block pressed"
<button @click="unblockUser"
class="pressed" >
@click="unblockUser" {{ $t('user_card.blocked') }}
> </button>
{{ $t('user_card.blocked') }} <button
</button> v-else
</span> class="btn btn-default btn-block"
<span v-if="!user.statusnet_blocking"> @click="blockUser"
<button @click="blockUser"> >
{{ $t('user_card.block') }} {{ $t('user_card.block') }}
</button> </button>
</span>
</div> </div>
<div
v-if="isOtherUser && loggedIn" <div>
class="block" <button
> class="btn btn-default btn-block"
<span> @click="reportUser"
<button @click="reportUser"> >
{{ $t('user_card.report') }} {{ $t('user_card.report') }}
</button> </button>
</span>
</div> </div>
<ModerationTools <ModerationTools
v-if="loggedIn.role === &quot;admin&quot;" v-if="loggedIn.role === &quot;admin&quot;"
:user="user" :user="user"
/> />
</div> </div>
<div
v-if="!loggedIn && user.is_local"
class="user-interactions"
>
<RemoteFollow :user="user" />
</div>
</div> </div>
</div> </div>
<div <div
@ -487,40 +506,22 @@
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
justify-content: space-between; justify-content: space-between;
margin-right: -.75em; margin-right: -.75em;
div { > * {
flex: 1 0 0; flex: 1 0 0;
margin-right: .75em; margin: 0 .75em .6em 0;
margin-bottom: .6em;
white-space: nowrap; white-space: nowrap;
} }
.mute {
max-width: 220px;
min-height: 28px;
}
.follow {
max-width: 220px;
min-height: 28px;
}
button { button {
width: 100%;
height: 100%;
margin: 0; margin: 0;
}
.remote-button { &.pressed {
height: 28px !important; // TODO: This should be themed.
width: 92%; border-bottom-color: rgba(255, 255, 255, 0.2);
} border-top-color: rgba(0, 0, 0, 0.2);
}
.pressed {
border-bottom-color: rgba(255, 255, 255, 0.2);
border-top-color: rgba(0, 0, 0, 0.2);
} }
} }
} }

View File

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

View File

@ -3,7 +3,6 @@ import UserCard from '../user_card/user_card.vue'
import FollowCard from '../follow_card/follow_card.vue' import FollowCard from '../follow_card/follow_card.vue'
import Timeline from '../timeline/timeline.vue' import Timeline from '../timeline/timeline.vue'
import Conversation from '../conversation/conversation.vue' import Conversation from '../conversation/conversation.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue'
import List from '../list/list.vue' import List from '../list/list.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more' import withLoadMore from '../../hocs/with_load_more/with_load_more'
@ -132,7 +131,6 @@ const UserProfile = {
Timeline, Timeline,
FollowerList, FollowerList,
FriendList, FriendList,
ModerationTools,
FollowCard, FollowCard,
Conversation Conversation
} }

View File

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

View File

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

View File

@ -17,7 +17,6 @@ import Autosuggest from '../autosuggest/autosuggest.vue'
import Importer from '../importer/importer.vue' import Importer from '../importer/importer.vue'
import Exporter from '../exporter/exporter.vue' import Exporter from '../exporter/exporter.vue'
import withSubscription from '../../hocs/with_subscription/with_subscription' import withSubscription from '../../hocs/with_subscription/with_subscription'
import userSearchApi from '../../services/new_api/user_search.js'
import Mfa from './mfa.vue' import Mfa from './mfa.vue'
const BlockList = withSubscription({ const BlockList = withSubscription({
@ -322,11 +321,8 @@ const UserSettings = {
}) })
}, },
queryUserIds (query) { queryUserIds (query) {
return userSearchApi.search({ query, store: this.$store }) return this.$store.dispatch('searchUsers', query)
.then((users) => { .then((users) => map(users, 'id'))
this.$store.dispatch('addNewUsers', users)
return map(users, 'id')
})
}, },
blockUsers (ids) { blockUsers (ids) {
return this.$store.dispatch('blockUsers', ids) return this.$store.dispatch('blockUsers', ids)

View File

@ -78,6 +78,7 @@
"timeline": "Timeline", "timeline": "Timeline",
"twkn": "The Whole Known Network", "twkn": "The Whole Known Network",
"user_search": "User Search", "user_search": "User Search",
"search": "Search",
"who_to_follow": "Who to follow", "who_to_follow": "Who to follow",
"preferences": "Preferences" "preferences": "Preferences"
}, },
@ -531,6 +532,8 @@
"remote_follow": "Remote follow", "remote_follow": "Remote follow",
"report": "Report", "report": "Report",
"statuses": "Statuses", "statuses": "Statuses",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"unblock": "Unblock", "unblock": "Unblock",
"unblock_progress": "Unblocking...", "unblock_progress": "Unblocking...",
"block_progress": "Blocking...", "block_progress": "Blocking...",
@ -595,5 +598,12 @@
"GiB": "GiB", "GiB": "GiB",
"TiB": "TiB" "TiB": "TiB"
} }
},
"search": {
"people": "People",
"hashtags": "Hashtags",
"person_talking": "{count} person talking",
"people_talking": "{count} people talking",
"no_results": "No results"
} }
} }

View File

@ -38,7 +38,8 @@
"interactions": "Взаимодействия", "interactions": "Взаимодействия",
"public_tl": "Публичная лента", "public_tl": "Публичная лента",
"timeline": "Лента", "timeline": "Лента",
"twkn": "Федеративная лента" "twkn": "Федеративная лента",
"search": "Поиск"
}, },
"notifications": { "notifications": {
"broken_favorite": "Неизвестный статус, ищем...", "broken_favorite": "Неизвестный статус, ищем...",
@ -381,5 +382,12 @@
}, },
"user_profile": { "user_profile": {
"timeline_title": "Лента пользователя" "timeline_title": "Лента пользователя"
},
"search": {
"people": "Люди",
"hashtags": "Хэштэги",
"person_talking": "Популярно у {count} человека",
"people_talking": "Популярно у {count} человек",
"no_results": "Ничего не найдено"
} }
} }

View File

@ -496,10 +496,19 @@ export const mutations = {
queueFlush (state, { timeline, id }) { queueFlush (state, { timeline, id }) {
state.timelines[timeline].flushMarker = 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] const newStatus = state.allStatusesObject[id]
newStatus.favoritedBy = favoritedByUsers.filter(_ => _) 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 }) { updateStatusWithPoll (state, { id, poll }) {
const status = state.allStatusesObject[id] const status = state.allStatusesObject[id]
@ -593,9 +602,26 @@ const statuses = {
Promise.all([ Promise.all([
rootState.api.backendInteractor.fetchFavoritedByUsers(id), rootState.api.backendInteractor.fetchFavoritedByUsers(id),
rootState.api.backendInteractor.fetchRebloggedByUsers(id) rootState.api.backendInteractor.fetchRebloggedByUsers(id)
]).then(([favoritedByUsers, rebloggedByUsers]) => ]).then(([favoritedByUsers, rebloggedByUsers]) => {
commit('addFavsAndRepeats', { id, 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 mutations

View File

@ -1,5 +1,4 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' 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 oauthApi from '../services/new_api/oauth.js'
import { compact, map, each, merge, last, concat, uniq } from 'lodash' import { compact, map, each, merge, last, concat, uniq } from 'lodash'
import { set } from 'vue' import { set } from 'vue'
@ -136,6 +135,7 @@ export const mutations = {
user.following = relationship.following user.following = relationship.following
user.muted = relationship.muting user.muted = relationship.muting
user.statusnet_blocking = relationship.blocking user.statusnet_blocking = relationship.blocking
user.subscribed = relationship.subscribing
} }
}) })
}, },
@ -305,6 +305,14 @@ const users = {
clearFollowers ({ commit }, userId) { clearFollowers ({ commit }, userId) {
commit('clearFollowers', 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) { registerPushNotifications (store) {
const token = store.state.currentUser.credentials const token = store.state.currentUser.credentials
const vapidPublicKey = store.rootState.instance.vapidPublicKey const vapidPublicKey = store.rootState.instance.vapidPublicKey
@ -356,14 +364,7 @@ const users = {
}) })
}, },
searchUsers (store, query) { searchUsers (store, query) {
// TODO: Move userSearch api into api.service return store.rootState.api.backendInteractor.searchUsers(query)
return userSearchApi.search({
query,
store: {
state: store.rootState,
getters: store.rootGetters
}
})
.then((users) => { .then((users) => {
store.commit('addNewUsers', users) store.commit('addNewUsers', users)
return users return users

View File

@ -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_UNBLOCK_USER_URL = id => `/api/v1/accounts/${id}/unblock`
const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute` const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute` 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_POST_STATUS_URL = '/api/v1/statuses'
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media' const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes` const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes`
@ -67,6 +69,7 @@ const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin`
const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin` const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin`
const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute` const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute` const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
const MASTODON_SEARCH_2 = `/api/v2/search`
const oldfetch = window.fetch const oldfetch = window.fetch
@ -78,7 +81,7 @@ let fetch = (url, options) => {
return oldfetch(fullUrl, options) return oldfetch(fullUrl, options)
} }
const promisedRequest = ({ method, url, payload, credentials, headers = {} }) => { const promisedRequest = ({ method, url, params, payload, credentials, headers = {} }) => {
const options = { const options = {
method, method,
headers: { headers: {
@ -87,6 +90,11 @@ const promisedRequest = ({ method, url, payload, credentials, headers = {} }) =>
...headers ...headers
} }
} }
if (params) {
url += '?' + Object.entries(params)
.map(([key, value]) => encodeURIComponent(key) + '=' + encodeURIComponent(value))
.join('&')
}
if (payload) { if (payload) {
options.body = JSON.stringify(payload) options.body = JSON.stringify(payload)
} }
@ -758,6 +766,14 @@ const unmuteUser = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_UNMUTE_USER_URL(id), credentials, method: 'POST' }) 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 }) => { const fetchBlocks = ({ credentials }) => {
return promisedRequest({ url: MASTODON_USER_BLOCKS_URL, credentials }) return promisedRequest({ url: MASTODON_USER_BLOCKS_URL, credentials })
.then((users) => users.map(parseUser)) .then((users) => users.map(parseUser))
@ -849,6 +865,48 @@ const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
}) })
} }
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 = { const apiService = {
verifyCredentials, verifyCredentials,
fetchTimeline, fetchTimeline,
@ -878,6 +936,8 @@ const apiService = {
fetchMutes, fetchMutes,
muteUser, muteUser,
unmuteUser, unmuteUser,
subscribeUser,
unsubscribeUser,
fetchBlocks, fetchBlocks,
fetchOAuthTokens, fetchOAuthTokens,
revokeOAuthToken, revokeOAuthToken,
@ -913,7 +973,8 @@ const apiService = {
fetchFavoritedByUsers, fetchFavoritedByUsers,
fetchRebloggedByUsers, fetchRebloggedByUsers,
reportUser, reportUser,
updateNotificationSettings updateNotificationSettings,
search2
} }
export default apiService export default apiService

View File

@ -108,6 +108,8 @@ const backendInteractorService = credentials => {
const fetchMutes = () => apiService.fetchMutes({ credentials }) const fetchMutes = () => apiService.fetchMutes({ credentials })
const muteUser = (id) => apiService.muteUser({ credentials, id }) const muteUser = (id) => apiService.muteUser({ credentials, id })
const unmuteUser = (id) => apiService.unmuteUser({ 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 fetchBlocks = () => apiService.fetchBlocks({ credentials })
const fetchFollowRequests = () => apiService.fetchFollowRequests({ credentials }) const fetchFollowRequests = () => apiService.fetchFollowRequests({ credentials })
const fetchOAuthTokens = () => apiService.fetchOAuthTokens({ credentials }) const fetchOAuthTokens = () => apiService.fetchOAuthTokens({ credentials })
@ -148,6 +150,8 @@ const backendInteractorService = credentials => {
const unfavorite = (id) => apiService.unfavorite({ id, credentials }) const unfavorite = (id) => apiService.unfavorite({ id, credentials })
const retweet = (id) => apiService.retweet({ id, credentials }) const retweet = (id) => apiService.retweet({ id, credentials })
const unretweet = (id) => apiService.unretweet({ 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 backendInteractorServiceInstance = { const backendInteractorServiceInstance = {
fetchStatus, fetchStatus,
@ -167,6 +171,8 @@ const backendInteractorService = credentials => {
fetchMutes, fetchMutes,
muteUser, muteUser,
unmuteUser, unmuteUser,
subscribeUser,
unsubscribeUser,
fetchBlocks, fetchBlocks,
fetchOAuthTokens, fetchOAuthTokens,
revokeOAuthToken, revokeOAuthToken,
@ -209,7 +215,8 @@ const backendInteractorService = credentials => {
unfavorite, unfavorite,
retweet, retweet,
unretweet, unretweet,
updateNotificationSettings updateNotificationSettings,
search2
} }
return backendInteractorServiceInstance return backendInteractorServiceInstance

View File

@ -68,6 +68,7 @@ export const parseUser = (data) => {
output.following = relationship.following output.following = relationship.following
output.statusnet_blocking = relationship.blocking output.statusnet_blocking = relationship.blocking
output.muted = relationship.muting output.muted = relationship.muting
output.subscribed = relationship.subscribing
} }
output.hide_follows = data.pleroma.hide_follows output.hide_follows = data.pleroma.hide_follows

View File

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

View File

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

0
static/font/README.txt Normal file → Executable file
View File

20
static/font/config.json Normal file → Executable file
View File

@ -150,12 +150,6 @@
"code": 61669, "code": 61669,
"src": "fontawesome" "src": "fontawesome"
}, },
{
"uid": "cd21cbfb28ad4d903cede582157f65dc",
"css": "bell",
"code": 59408,
"src": "fontawesome"
},
{ {
"uid": "ccc2329632396dc096bb638d4b46fb98", "uid": "ccc2329632396dc096bb638d4b46fb98",
"css": "mail-alt", "css": "mail-alt",
@ -277,6 +271,20 @@
"search": [ "search": [
"ellipsis" "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"
]
} }
] ]
} }

View File

@ -15,7 +15,7 @@
.icon-right-open:before { content: '\e80d'; } /* '' */ .icon-right-open:before { content: '\e80d'; } /* '' */
.icon-left-open:before { content: '\e80e'; } /* '' */ .icon-left-open:before { content: '\e80e'; } /* '' */
.icon-up-open:before { content: '\e80f'; } /* '' */ .icon-up-open:before { content: '\e80f'; } /* '' */
.icon-bell:before { content: '\e810'; } /* '' */ .icon-bell-ringing-o:before { content: '\e810'; } /* '' */
.icon-lock:before { content: '\e811'; } /* '' */ .icon-lock:before { content: '\e811'; } /* '' */
.icon-globe:before { content: '\e812'; } /* '' */ .icon-globe:before { content: '\e812'; } /* '' */
.icon-brush:before { content: '\e813'; } /* '' */ .icon-brush:before { content: '\e813'; } /* '' */

File diff suppressed because one or more lines are too long

View File

@ -15,7 +15,7 @@
.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80d;&nbsp;'); } .icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80d;&nbsp;'); }
.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80e;&nbsp;'); } .icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80e;&nbsp;'); }
.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80f;&nbsp;'); } .icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80f;&nbsp;'); }
.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); } .icon-bell-ringing-o { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe811;&nbsp;'); } .icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe811;&nbsp;'); }
.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe812;&nbsp;'); } .icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe812;&nbsp;'); }
.icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); } .icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); }

View File

@ -26,7 +26,7 @@
.icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80d;&nbsp;'); } .icon-right-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80d;&nbsp;'); }
.icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80e;&nbsp;'); } .icon-left-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80e;&nbsp;'); }
.icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80f;&nbsp;'); } .icon-up-open { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80f;&nbsp;'); }
.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); } .icon-bell-ringing-o { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe811;&nbsp;'); } .icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe811;&nbsp;'); }
.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe812;&nbsp;'); } .icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe812;&nbsp;'); }
.icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); } .icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); }

View File

@ -1,11 +1,11 @@
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('../font/fontello.eot?3304725'); src: url('../font/fontello.eot?91349539');
src: url('../font/fontello.eot?3304725#iefix') format('embedded-opentype'), src: url('../font/fontello.eot?91349539#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?3304725') format('woff2'), url('../font/fontello.woff2?91349539') format('woff2'),
url('../font/fontello.woff?3304725') format('woff'), url('../font/fontello.woff?91349539') format('woff'),
url('../font/fontello.ttf?3304725') format('truetype'), url('../font/fontello.ttf?91349539') format('truetype'),
url('../font/fontello.svg?3304725#fontello') format('svg'); url('../font/fontello.svg?91349539#fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('../font/fontello.svg?3304725#fontello') format('svg'); src: url('../font/fontello.svg?91349539#fontello') format('svg');
} }
} }
*/ */
@ -71,7 +71,7 @@
.icon-right-open:before { content: '\e80d'; } /* '' */ .icon-right-open:before { content: '\e80d'; } /* '' */
.icon-left-open:before { content: '\e80e'; } /* '' */ .icon-left-open:before { content: '\e80e'; } /* '' */
.icon-up-open:before { content: '\e80f'; } /* '' */ .icon-up-open:before { content: '\e80f'; } /* '' */
.icon-bell:before { content: '\e810'; } /* '' */ .icon-bell-ringing-o:before { content: '\e810'; } /* '' */
.icon-lock:before { content: '\e811'; } /* '' */ .icon-lock:before { content: '\e811'; } /* '' */
.icon-globe:before { content: '\e812'; } /* '' */ .icon-globe:before { content: '\e812'; } /* '' */
.icon-brush:before { content: '\e813'; } /* '' */ .icon-brush:before { content: '\e813'; } /* '' */

12
static/font/demo.html Normal file → Executable file
View File

@ -229,11 +229,11 @@ body {
} }
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('./font/fontello.eot?14310629'); src: url('./font/fontello.eot?82370835');
src: url('./font/fontello.eot?14310629#iefix') format('embedded-opentype'), src: url('./font/fontello.eot?82370835#iefix') format('embedded-opentype'),
url('./font/fontello.woff?14310629') format('woff'), url('./font/fontello.woff?82370835') format('woff'),
url('./font/fontello.ttf?14310629') format('truetype'), url('./font/fontello.ttf?82370835') format('truetype'),
url('./font/fontello.svg?14310629#fontello') format('svg'); url('./font/fontello.svg?82370835#fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -322,7 +322,7 @@ body {
<div class="the-icons span3" title="Code: 0xe80f"><i class="demo-icon icon-up-open">&#xe80f;</i> <span class="i-name">icon-up-open</span><span class="i-code">0xe80f</span></div> <div class="the-icons span3" title="Code: 0xe80f"><i class="demo-icon icon-up-open">&#xe80f;</i> <span class="i-name">icon-up-open</span><span class="i-code">0xe80f</span></div>
</div> </div>
<div class="row"> <div class="row">
<div class="the-icons span3" title="Code: 0xe810"><i class="demo-icon icon-bell">&#xe810;</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">&#xe810;</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">&#xe811;</i> <span class="i-name">icon-lock</span><span class="i-code">0xe811</span></div> <div class="the-icons span3" title="Code: 0xe811"><i class="demo-icon icon-lock">&#xe811;</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">&#xe812;</i> <span class="i-name">icon-globe</span><span class="i-code">0xe812</span></div> <div class="the-icons span3" title="Code: 0xe812"><i class="demo-icon icon-globe">&#xe812;</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">&#xe813;</i> <span class="i-name">icon-brush</span><span class="i-code">0xe813</span></div> <div class="the-icons span3" title="Code: 0xe813"><i class="demo-icon icon-brush">&#xe813;</i> <span class="i-name">icon-brush</span><span class="i-code">0xe813</span></div>

Binary file not shown.

View File

@ -38,7 +38,7 @@
<glyph glyph-name="up-open" unicode="&#xe80f;" 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="up-open" unicode="&#xe80f;" 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="&#xe810;" 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="&#xe810;" 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="&#xe811;" 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" /> <glyph glyph-name="lock" unicode="&#xe811;" 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.