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:
Henry Jameson 2019-07-25 13:05:59 +03:00
commit bbcd3190f2
62 changed files with 1347 additions and 557 deletions

View file

@ -25,14 +25,13 @@
"localforage": "^1.5.0", "localforage": "^1.5.0",
"object-path": "^0.11.3", "object-path": "^0.11.3",
"phoenix": "^1.3.0", "phoenix": "^1.3.0",
"popper.js": "^1.14.7",
"portal-vue": "^2.1.4", "portal-vue": "^2.1.4",
"sanitize-html": "^1.13.0", "sanitize-html": "^1.13.0",
"v-click-outside": "^2.1.1", "v-click-outside": "^2.1.1",
"v-tooltip": "^2.0.2",
"vue": "^2.5.13", "vue": "^2.5.13",
"vue-chat-scroll": "^1.2.1", "vue-chat-scroll": "^1.2.1",
"vue-i18n": "^7.3.2", "vue-i18n": "^7.3.2",
"vue-popperjs": "^2.0.3",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-template-compiler": "^2.3.4", "vue-template-compiler": "^2.3.4",
"vuelidate": "^0.7.4", "vuelidate": "^0.7.4",
@ -81,8 +80,8 @@
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"karma": "^3.0.0", "karma": "^3.0.0",
"karma-coverage": "^1.1.1", "karma-coverage": "^1.1.1",
"karma-mocha": "^1.2.0",
"karma-firefox-launcher": "^1.1.0", "karma-firefox-launcher": "^1.1.0",
"karma-mocha": "^1.2.0",
"karma-sinon-chai": "^2.0.2", "karma-sinon-chai": "^2.0.2",
"karma-sourcemap-loader": "^0.3.7", "karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.26", "karma-spec-reporter": "0.0.26",

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

@ -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 }) => { const getStaticEmoji = async ({ store }) => {
try { try {
const res = await window.fetch('/static/emoji.json') const res = await window.fetch('/static/emoji.json')
@ -286,6 +317,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
setConfig({ store }), setConfig({ store }),
getTOS({ store }), getTOS({ store }),
getInstancePanel({ store }), getInstancePanel({ store }),
getStickers({ store }),
getStaticEmoji({ store }), getStaticEmoji({ store }),
getCustomEmoji({ store }), getCustomEmoji({ store }),
getNodeInfo({ store }) getNodeInfo({ store })

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'
@ -19,6 +19,14 @@ import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
import About from 'components/about/about.vue' import About from 'components/about/about.vue'
export default (store) => { export default (store) => {
const validateAuthenticatedRoute = (to, from, next) => {
if (store.state.users.currentUser) {
next()
} else {
next(store.state.instance.redirectRootNoLogin || '/main/all')
}
}
return [ return [
{ name: 'root', { name: 'root',
path: '/', path: '/',
@ -30,23 +38,23 @@ export default (store) => {
}, },
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline }, { name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline }, { 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: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile }, { name: 'external-user-profile', path: '/users/:id', component: UserProfile },
{ name: 'interactions', path: '/users/:username/interactions', component: Interactions }, { name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
{ name: 'dms', path: '/users/:username/dms', component: DMs }, { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'settings', path: '/settings', component: Settings }, { name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration }, { name: 'registration', path: '/registration', component: Registration },
{ name: 'registration-token', path: '/registration/:token', component: Registration }, { name: 'registration-token', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
{ name: 'user-settings', path: '/user-settings', component: UserSettings }, { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },
{ name: 'notifications', path: '/:username/notifications', component: Notifications }, { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
{ 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, beforeEnter: validateAuthenticatedRoute },
{ 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,20 +1,27 @@
import { debounce } from 'lodash'
/** /**
* suggest - generates a suggestor function to be used by emoji-input * suggest - generates a suggestor function to be used by emoji-input
* data: object providing source information for specific types of suggestions: * data: object providing source information for specific types of suggestions:
* data.emoji - optional, an array of all emoji available i.e. * data.emoji - optional, an array of all emoji available i.e.
* (state.instance.emoji + state.instance.customEmoji) * (state.instance.emoji + state.instance.customEmoji)
* data.users - optional, an array of all known users * 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 * 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. * 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 => { export default data => input => {
const firstChar = input[0] const firstChar = input[0]
if (firstChar === ':' && data.emoji) { if (firstChar === ':' && data.emoji) {
return suggestEmoji(data.emoji)(input) return suggestEmoji(data.emoji)(input)
} }
if (firstChar === '@' && data.users) { if (firstChar === '@' && data.users) {
return suggestUsers(data.users)(input) return suggestUsers(data)(input)
} }
return [] 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) const noPrefix = input.toLowerCase().substr(1)
return users.filter( const users = data.users
const newUsers = users.filter(
user => user =>
user.screen_name.toLowerCase().startsWith(noPrefix) || user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix) user.name.toLowerCase().startsWith(noPrefix)
@ -75,5 +84,11 @@ export const suggestUsers = users => input => {
imageUrl: profile_image_url_original, imageUrl: profile_image_url_original,
replacement: '@' + screen_name + ' ' 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 */ /* eslint-enable camelcase */
} }

View file

@ -1,45 +1,21 @@
import Popper from 'vue-popperjs/src/component/popper.js.vue'
const ExtraButtons = { const ExtraButtons = {
props: [ 'status' ], props: [ 'status' ],
components: {
Popper
},
data () {
return {
showDropDown: false,
showPopper: true
}
},
methods: { methods: {
deleteStatus () { deleteStatus () {
this.refreshPopper()
const confirmed = window.confirm(this.$t('status.delete_confirm')) const confirmed = window.confirm(this.$t('status.delete_confirm'))
if (confirmed) { if (confirmed) {
this.$store.dispatch('deleteStatus', { id: this.status.id }) this.$store.dispatch('deleteStatus', { id: this.status.id })
} }
}, },
toggleMenu () {
this.showDropDown = !this.showDropDown
},
pinStatus () { pinStatus () {
this.refreshPopper()
this.$store.dispatch('pinStatus', this.status.id) this.$store.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch(err => this.$emit('onError', err.error.error))
}, },
unpinStatus () { unpinStatus () {
this.refreshPopper()
this.$store.dispatch('unpinStatus', this.status.id) this.$store.dispatch('unpinStatus', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error)) .catch(err => this.$emit('onError', err.error.error))
},
refreshPopper () {
this.showPopper = false
this.showDropDown = false
setTimeout(() => {
this.showPopper = true
})
} }
}, },
computed: { computed: {

View file

@ -1,21 +1,17 @@
<template> <template>
<Popper <v-popover
v-if="enabled && showPopper" v-if="enabled"
trigger="click" trigger="click"
append-to-body placement="top"
:options="{ class="extra-button-popover"
placement: 'top', :offset="5"
modifiers: { :container="false"
arrow: { enabled: true },
offset: { offset: '0, 5px' },
}
}"
@hide="showDropDown = false"
> >
<div class="popper-wrapper"> <div slot="popover">
<div class="dropdown-menu"> <div class="dropdown-menu">
<button <button
v-if="!status.pinned && canPin" v-if="!status.pinned && canPin"
v-close-popover
class="dropdown-item dropdown-item-icon" class="dropdown-item dropdown-item-icon"
@click.prevent="pinStatus" @click.prevent="pinStatus"
> >
@ -23,6 +19,7 @@
</button> </button>
<button <button
v-if="status.pinned && canPin" v-if="status.pinned && canPin"
v-close-popover
class="dropdown-item dropdown-item-icon" class="dropdown-item dropdown-item-icon"
@click.prevent="unpinStatus" @click.prevent="unpinStatus"
> >
@ -30,6 +27,7 @@
</button> </button>
<button <button
v-if="canDelete" v-if="canDelete"
v-close-popover
class="dropdown-item dropdown-item-icon" class="dropdown-item dropdown-item-icon"
@click.prevent="deleteStatus" @click.prevent="deleteStatus"
> >
@ -37,17 +35,10 @@
</button> </button>
</div> </div>
</div> </div>
<div <div class="button-icon">
slot="reference" <i class="icon-ellipsis" />
class="button-icon"
@click="toggleMenu"
>
<i
class="icon-ellipsis"
:class="{'icon-clicked': showDropDown}"
/>
</div> </div>
</Popper> </v-popover>
</template> </template>
<script src="./extra_buttons.js" ></script> <script src="./extra_buttons.js" ></script>
@ -59,7 +50,8 @@
.icon-ellipsis { .icon-ellipsis {
cursor: pointer; cursor: pointer;
&:hover, &.icon-clicked { &:hover,
.extra-button-popover.open & {
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
} }

View file

@ -1,5 +1,4 @@
import DialogModal from '../dialog_modal/dialog_modal.vue' 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 FORCE_NSFW = 'mrf_tag:media-force-nsfw'
const STRIP_MEDIA = 'mrf_tag:media-strip' const STRIP_MEDIA = 'mrf_tag:media-strip'
@ -29,8 +28,7 @@ const ModerationTools = {
} }
}, },
components: { components: {
DialogModal, DialogModal
Popper
}, },
computed: { computed: {
tagsSet () { tagsSet () {
@ -41,9 +39,6 @@ const ModerationTools = {
} }
}, },
methods: { methods: {
toggleMenu () {
this.showDropDown = !this.showDropDown
},
hasTag (tagName) { hasTag (tagName) {
return this.tagsSet.has(tagName) return this.tagsSet.has(tagName)
}, },

View file

@ -1,21 +1,15 @@
<template> <template>
<div <div>
class="block" <v-popover
style="position: relative"
>
<Popper
trigger="click" trigger="click"
append-to-body class="moderation-tools-popover"
:options="{ :container="false"
placement: 'bottom-end', placement="bottom-end"
modifiers: { :offset="5"
arrow: { enabled: true }, @show="showDropDown = true"
offset: { offset: '0, 5px' },
}
}"
@hide="showDropDown = false" @hide="showDropDown = false"
> >
<div class="popper-wrapper"> <div slot="popover">
<div class="dropdown-menu"> <div class="dropdown-menu">
<span v-if="user.is_local"> <span v-if="user.is_local">
<button <button
@ -130,13 +124,12 @@
</div> </div>
</div> </div>
<button <button
slot="reference" class="btn btn-default btn-block"
:class="{ pressed: showDropDown }" :class="{ pressed: showDropDown }"
@click="toggleMenu"
> >
{{ $t('user_card.admin_menu.moderation') }} {{ $t('user_card.admin_menu.moderation') }}
</button> </button>
</Popper> </v-popover>
<portal to="modal"> <portal to="modal">
<DialogModal <DialogModal
v-if="showDeleteUserDialog" v-if="showDeleteUserDialog"
@ -190,4 +183,11 @@
} }
} }
.moderation-tools-popover {
height: 100%;
.trigger {
display: flex !important;
height: 100%;
}
}
</style> </style>

View file

@ -1,71 +1,99 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.popper-wrapper { .tooltip.popover {
z-index: 8; z-index: 8;
}
.popper-wrapper .popper__arrow { .popover-inner {
width: 0; box-shadow: 1px 1px 4px rgba(0,0,0,.6);
height: 0; box-shadow: var(--panelShadow);
border-style: solid; border-radius: $fallback--btnRadius;
position: absolute; border-radius: var(--btnRadius, $fallback--btnRadius);
margin: 5px; background-color: $fallback--bg;
} background-color: var(--bg, $fallback--bg);
}
.popper-wrapper[x-placement^="top"] { .popover-arrow {
margin-bottom: 5px; 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 { &[x-placement^="top"] {
border-width: 5px 5px 0 5px; margin-bottom: 5px;
border-color: $fallback--bg transparent transparent transparent;
border-color: var(--bg, $fallback--bg) transparent transparent transparent;
bottom: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
.popper-wrapper[x-placement^="bottom"] { .popover-arrow {
margin-top: 5px; 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 { &[x-placement^="bottom"] {
border-width: 0 5px 5px 5px; margin-top: 5px;
border-color: transparent transparent $fallback--bg transparent;
border-color: transparent transparent var(--bg, $fallback--bg) transparent;
top: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
.popper-wrapper[x-placement^="right"] { .popover-arrow {
margin-left: 5px; 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 { &[x-placement^="right"] {
border-width: 5px 5px 5px 0; margin-left: 5px;
border-color: transparent $fallback--bg transparent transparent;
border-color: transparent var(--bg, $fallback--bg) transparent transparent;
left: -5px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}
.popper-wrapper[x-placement^="left"] { .popover-arrow {
margin-right: 5px; 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 { &[x-placement^="left"] {
border-width: 5px 0 5px 5px; margin-right: 5px;
border-color: transparent transparent transparent $fallback--bg;
border-color: transparent transparent transparent var(--bg, $fallback--bg); .popover-arrow {
right: -5px; border-width: 5px 0 5px 5px;
top: calc(50% - 5px); border-top-color: transparent !important;
margin-left: 0; border-right-color: transparent !important;
margin-right: 0; 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 { .dropdown-menu {
@ -76,13 +104,6 @@
list-style: none; list-style: none;
max-width: 100vw; max-width: 100vw;
z-index: 10; 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 { .dropdown-divider {
height: 0; height: 0;

View file

@ -3,6 +3,7 @@ import MediaUpload from '../media_upload/media_upload.vue'
import ScopeSelector from '../scope_selector/scope_selector.vue' import ScopeSelector from '../scope_selector/scope_selector.vue'
import EmojiInput from '../emoji-input/emoji-input.vue' import EmojiInput from '../emoji-input/emoji-input.vue'
import PollForm from '../poll/poll_form.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 fileTypeService from '../../services/file_type/file_type.service.js'
import { reject, map, uniqBy } from 'lodash' import { reject, map, uniqBy } from 'lodash'
import suggestor from '../emoji-input/suggestor.js' import suggestor from '../emoji-input/suggestor.js'
@ -34,6 +35,7 @@ const PostStatusForm = {
MediaUpload, MediaUpload,
EmojiInput, EmojiInput,
PollForm, PollForm,
StickerPicker,
ScopeSelector ScopeSelector
}, },
mounted () { mounted () {
@ -82,7 +84,8 @@ const PostStatusForm = {
contentType contentType
}, },
caret: 0, caret: 0,
pollFormVisible: false pollFormVisible: false,
stickerPickerVisible: false
} }
}, },
computed: { computed: {
@ -104,7 +107,8 @@ const PostStatusForm = {
...this.$store.state.instance.emoji, ...this.$store.state.instance.emoji,
...this.$store.state.instance.customEmoji ...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 () { emojiSuggestor () {
@ -157,6 +161,12 @@ const PostStatusForm = {
safeDMEnabled () { safeDMEnabled () {
return this.$store.state.instance.safeDM return this.$store.state.instance.safeDM
}, },
stickersAvailable () {
if (this.$store.state.instance.stickers) {
return this.$store.state.instance.stickers.length > 0
}
return 0
},
pollsAvailable () { pollsAvailable () {
return this.$store.state.instance.pollsAvailable && return this.$store.state.instance.pollsAvailable &&
this.$store.state.instance.pollLimits.max_options >= 2 this.$store.state.instance.pollLimits.max_options >= 2
@ -212,6 +222,7 @@ const PostStatusForm = {
poll: {} poll: {}
} }
this.pollFormVisible = false this.pollFormVisible = false
this.stickerPickerVisible = false
this.$refs.mediaUpload.clearFile() this.$refs.mediaUpload.clearFile()
this.clearPollForm() this.clearPollForm()
this.$emit('posted') this.$emit('posted')
@ -228,6 +239,7 @@ const PostStatusForm = {
addMediaFile (fileInfo) { addMediaFile (fileInfo) {
this.newStatus.files.push(fileInfo) this.newStatus.files.push(fileInfo)
this.enableSubmit() this.enableSubmit()
this.stickerPickerVisible = false
}, },
removeMediaFile (fileInfo) { removeMediaFile (fileInfo) {
let index = this.newStatus.files.indexOf(fileInfo) let index = this.newStatus.files.indexOf(fileInfo)
@ -287,6 +299,14 @@ const PostStatusForm = {
changeVis (visibility) { changeVis (visibility) {
this.newStatus.visibility = visibility this.newStatus.visibility = visibility
}, },
toggleStickerPicker () {
this.stickerPickerVisible = !this.stickerPickerVisible
},
clearStickerPicker () {
if (this.$refs.stickerPicker) {
this.$refs.stickerPicker.clear()
}
},
togglePollForm () { togglePollForm () {
this.pollFormVisible = !this.pollFormVisible this.pollFormVisible = !this.pollFormVisible
}, },

View file

@ -157,6 +157,17 @@
@uploaded="addMediaFile" @uploaded="addMediaFile"
@upload-failed="uploadFailed" @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 <div
v-if="pollsAvailable" v-if="pollsAvailable"
class="poll-icon" class="poll-icon"
@ -169,7 +180,6 @@
/> />
</div> </div>
</div> </div>
<button <button
v-if="posting" v-if="posting"
disabled disabled
@ -248,6 +258,11 @@
<label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label> <label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label>
</div> </div>
</form> </form>
<sticker-picker
v-if="stickerPickerVisible"
ref="stickerPicker"
@uploaded="addMediaFile"
/>
</div> </div>
</template> </template>
@ -310,7 +325,7 @@
} }
} }
.poll-icon { .poll-icon, .sticker-icon {
font-size: 26px; font-size: 26px;
flex: 1; flex: 1;
@ -320,6 +335,11 @@
} }
} }
.sticker-icon {
flex: 0;
min-width: 50px;
}
.icon-chart-bar { .icon-chart-bar {
cursor: pointer; cursor: pointer;
} }

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

@ -110,8 +110,9 @@ const Status = {
}, },
muteWordHits () { muteWordHits () {
const statusText = this.status.text.toLowerCase() const statusText = this.status.text.toLowerCase()
const statusSummary = this.status.summary.toLowerCase()
const hits = filter(this.muteWords, (muteWord) => { const hits = filter(this.muteWords, (muteWord) => {
return statusText.includes(muteWord.toLowerCase()) return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase())
}) })
return hits return hits
@ -280,6 +281,11 @@ const Status = {
}, },
tags () { tags () {
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ') 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: { components: {
@ -316,11 +322,8 @@ const Status = {
this.error = undefined this.error = undefined
}, },
linkClicked (event) { linkClicked (event) {
let { target } = event const target = event.target.closest('.status-content a')
if (target.tagName === 'SPAN') { if (target) {
target = target.parentNode
}
if (target.tagName === 'A') {
if (target.className.match(/mention/)) { if (target.className.match(/mention/)) {
const href = target.href const href = target.href
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, 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) 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

@ -344,7 +344,7 @@
<transition name="fade"> <transition name="fade">
<div <div
v-if="isFocused && combinedFavsAndRepeatsUsers.length > 0" v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0"
class="favs-repeated-users" class="favs-repeated-users"
> >
<div class="stats"> <div class="stats">
@ -820,11 +820,12 @@ $status-margin: 0.75em;
} }
.status-actions { .status-actions {
position: relative;
width: 100%; width: 100%;
display: flex; display: flex;
margin-top: $status-margin; margin-top: $status-margin;
div, favorite-button { > * {
max-width: 4em; max-width: 4em;
flex: 1; flex: 1;
} }

View 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

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

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,11 +41,23 @@ 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')
} }
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 ( return (
<div class={ classesWrapper.join(' ')}> <div class={ classesWrapper.join(' ')}>
<button <button

View file

@ -53,6 +53,12 @@
background: transparent; background: transparent;
z-index: 5; z-index: 5;
} }
img {
max-height: 26px;
vertical-align: top;
margin-top: -5px;
}
} }
&:not(.active) { &:not(.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
@ -264,7 +283,6 @@
.user-card { .user-card {
background-size: cover; background-size: cover;
overflow: hidden;
.panel-heading { .panel-heading {
padding: .5em 0; padding: .5em 0;
@ -279,6 +297,8 @@
word-wrap: break-word; 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), $fallback--bg 80%);
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $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 { p {
@ -484,43 +504,26 @@
} }
} }
.user-interactions { .user-interactions {
position: relative;
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({
@ -92,7 +91,8 @@ const UserSettings = {
...this.$store.state.instance.emoji, ...this.$store.state.instance.emoji,
...this.$store.state.instance.customEmoji ...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 () { emojiSuggestor () {
@ -322,11 +322,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"
}, },
@ -105,6 +106,9 @@
"expired": "Poll ended {0} ago", "expired": "Poll ended {0} ago",
"not_enough_options": "Too few unique options in poll" "not_enough_options": "Too few unique options in poll"
}, },
"stickers": {
"add_sticker": "Add Sticker"
},
"interactions": { "interactions": {
"favs_repeats": "Repeats and Favorites", "favs_repeats": "Repeats and Favorites",
"follows": "New follows", "follows": "New follows",
@ -529,6 +533,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...",
@ -593,5 +599,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

@ -27,7 +27,11 @@
"optional": "opcional", "optional": "opcional",
"show_more": "Mostrar más", "show_more": "Mostrar más",
"show_less": "Mostrar menos", "show_less": "Mostrar menos",
"cancel": "Cancelar" "cancel": "Cancelar",
"disable": "Inhabilitar",
"enable": "Habilitar",
"confirm": "Confirmar",
"verify": "Verificar"
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "Recortar la foto", "crop_picture": "Recortar la foto",
@ -48,7 +52,15 @@
"placeholder": "p.ej. lain", "placeholder": "p.ej. lain",
"register": "Registrar", "register": "Registrar",
"username": "Usuario", "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": { "media_modal": {
"previous": "Anterior", "previous": "Anterior",
@ -60,11 +72,13 @@
"chat": "Chat Local", "chat": "Chat Local",
"friend_requests": "Solicitudes de amistad", "friend_requests": "Solicitudes de amistad",
"mentions": "Menciones", "mentions": "Menciones",
"interactions": "Interacciones",
"dms": "Mensajes Directo", "dms": "Mensajes Directo",
"public_tl": "Línea Temporal Pública", "public_tl": "Línea Temporal Pública",
"timeline": "Línea Temporal", "timeline": "Línea Temporal",
"twkn": "Toda La Red Conocida", "twkn": "Toda La Red Conocida",
"user_search": "Búsqueda de Usuarios", "user_search": "Búsqueda de Usuarios",
"search": "Buscar",
"who_to_follow": "A quién seguir", "who_to_follow": "A quién seguir",
"preferences": "Preferencias" "preferences": "Preferencias"
}, },
@ -78,6 +92,25 @@
"repeated_you": "repite tu estado", "repeated_you": "repite tu estado",
"no_more_notifications": "No hay más notificaciones" "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": { "post_status": {
"new_status": "Publicar un nuevo estado", "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.", "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)", "content_warning": "Tema (opcional)",
"default": "Acabo de aterrizar en L.A.", "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.", "direct_warning_to_first_only": "Esta publicación solo será visible para los usuarios mencionados al comienzo del mensaje.",
"posting": "Publicando", "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": { "scope": {
"direct": "Directo - Solo para los usuarios mencionados.", "direct": "Directo - Solo para los usuarios mencionados.",
"private": "Solo-Seguidores - Solo tus seguidores leeran la publicación", "private": "Solo-Seguidores - Solo tus seguidores leeran la publicación",
@ -127,6 +165,29 @@
}, },
"settings": { "settings": {
"app_name": "Nombre de la aplicación", "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", "attachmentRadius": "Adjuntos",
"attachments": "Adjuntos", "attachments": "Adjuntos",
"autoload": "Activar carga automática al llegar al final de la página", "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_all": "Mostrar todas las réplicas",
"reply_visibility_following": "Solo mostrar réplicas para mí o usuarios a los que sigo", "reply_visibility_following": "Solo mostrar réplicas para mí o usuarios a los que sigo",
"reply_visibility_self": "Solo mostrar réplicas para mí", "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_err": "Error al guardar los ajustes",
"saving_ok": "Ajustes guardados", "saving_ok": "Ajustes guardados",
"search_user_to_block": "Buscar usuarios a bloquear", "search_user_to_block": "Buscar usuarios a bloquear",
@ -265,6 +327,13 @@
"true": "sí" "true": "sí"
}, },
"notifications": "Notificaciones", "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", "enable_web_push_notifications": "Habilitar las notificiaciones en el navegador",
"style": { "style": {
"switcher": { "switcher": {
@ -381,6 +450,40 @@
"frontend_version": "Versión del Frontend" "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": { "timeline": {
"collapse": "Colapsar", "collapse": "Colapsar",
"conversation": "Conversación", "conversation": "Conversación",
@ -396,6 +499,11 @@
"status": { "status": {
"favorites": "Favoritos", "favorites": "Favoritos",
"repeats": "Repetidos", "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", "reply_to": "Responder a",
"replies_list": "Respuestas:" "replies_list": "Respuestas:"
}, },
@ -422,6 +530,8 @@
"remote_follow": "Seguir", "remote_follow": "Seguir",
"report": "Reportar", "report": "Reportar",
"statuses": "Estados", "statuses": "Estados",
"subscribe": "Suscribirse",
"unsubscribe": "Desuscribirse",
"unblock": "Desbloquear", "unblock": "Desbloquear",
"unblock_progress": "Desbloqueando...", "unblock_progress": "Desbloqueando...",
"block_progress": "Bloqueando...", "block_progress": "Bloqueando...",
@ -486,5 +596,12 @@
"GiB": "GiB", "GiB": "GiB",
"TiB": "TiB" "TiB": "TiB"
} }
},
"search": {
"people": "Personas",
"hashtags": "Hashtags",
"person_talking": "{count} personas hablando",
"people_talking": "{count} gente hablando",
"no_results": "Sin resultados"
} }
} }

View file

@ -27,7 +27,11 @@
"optional": "かかなくてもよい", "optional": "かかなくてもよい",
"show_more": "つづきをみる", "show_more": "つづきをみる",
"show_less": "たたむ", "show_less": "たたむ",
"cancel": "キャンセル" "cancel": "キャンセル",
"disable": "なし",
"enable": "あり",
"confirm": "たしかめる",
"verify": "たしかめる"
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "がぞうをきりぬく", "crop_picture": "がぞうをきりぬく",
@ -48,7 +52,15 @@
"placeholder": "れい: lain", "placeholder": "れい: lain",
"register": "はじめる", "register": "はじめる",
"username": "ユーザーめい", "username": "ユーザーめい",
"hint": "はなしあいにくわわるには、ログインしてください" "hint": "はなしあいにくわわるには、ログインしてください",
"authentication_code": "にんしょうコード",
"enter_recovery_code": "リカバリーコードをいれてください",
"enter_two_factor_code": "2-ファクターコードをいれてください",
"recovery_code": "リカバリーコード",
"heading" : {
"totp" : "2-ファクターにんしょう",
"recovery" : "2-ファクターリカバリー"
}
}, },
"media_modal": { "media_modal": {
"previous": "まえ", "previous": "まえ",
@ -79,6 +91,20 @@
"repeated_you": "あなたのステータスがリピートされました", "repeated_you": "あなたのステータスがリピートされました",
"no_more_notifications": "つうちはありません" "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": { "interactions": {
"favs_repeats": "リピートとおきにいり", "favs_repeats": "リピートとおきにいり",
"follows": "あたらしいフォロー", "follows": "あたらしいフォロー",
@ -139,6 +165,29 @@
}, },
"settings": { "settings": {
"app_name": "アプリのなまえ", "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": "ファイル", "attachmentRadius": "ファイル",
"attachments": "ファイル", "attachments": "ファイル",
"autoload": "したにスクロールしたとき、じどうてきによみこむ。", "autoload": "したにスクロールしたとき、じどうてきによみこむ。",

View file

@ -27,7 +27,11 @@
"optional": "省略可", "optional": "省略可",
"show_more": "もっと見る", "show_more": "もっと見る",
"show_less": "たたむ", "show_less": "たたむ",
"cancel": "キャンセル" "cancel": "キャンセル",
"disable": "無効",
"enable": "有効",
"confirm": "確認",
"verify": "検査"
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "画像を切り抜く", "crop_picture": "画像を切り抜く",
@ -48,7 +52,15 @@
"placeholder": "例: lain", "placeholder": "例: lain",
"register": "登録", "register": "登録",
"username": "ユーザー名", "username": "ユーザー名",
"hint": "会話に加わるには、ログインしてください" "hint": "会話に加わるには、ログインしてください",
"authentication_code": "認証コード",
"enter_recovery_code": "リカバリーコードを入力してください",
"enter_two_factor_code": "2段階認証コードを入力してください",
"recovery_code": "リカバリーコード",
"heading" : {
"totp" : "2段階認証",
"recovery" : "2段階リカバリー"
}
}, },
"media_modal": { "media_modal": {
"previous": "前", "previous": "前",
@ -79,6 +91,20 @@
"repeated_you": "あなたのステータスがリピートされました", "repeated_you": "あなたのステータスがリピートされました",
"no_more_notifications": "通知はありません" "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": { "interactions": {
"favs_repeats": "リピートとお気に入り", "favs_repeats": "リピートとお気に入り",
"follows": "新しいフォロワー", "follows": "新しいフォロワー",
@ -139,6 +165,29 @@
}, },
"settings": { "settings": {
"app_name": "アプリの名称", "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": "ファイル", "attachmentRadius": "ファイル",
"attachments": "ファイル", "attachments": "ファイル",
"autoload": "下にスクロールしたとき、自動的に読み込む。", "autoload": "下にスクロールしたとき、自動的に読み込む。",

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

@ -26,6 +26,7 @@ import messages from './i18n/messages.js'
import VueChatScroll from 'vue-chat-scroll' import VueChatScroll from 'vue-chat-scroll'
import VueClickOutside from 'v-click-outside' import VueClickOutside from 'v-click-outside'
import PortalVue from 'portal-vue' import PortalVue from 'portal-vue'
import VTooltip from 'v-tooltip'
import afterStoreSetup from './boot/after_store.js' import afterStoreSetup from './boot/after_store.js'
@ -37,6 +38,7 @@ Vue.use(VueI18n)
Vue.use(VueChatScroll) Vue.use(VueChatScroll)
Vue.use(VueClickOutside) Vue.use(VueClickOutside)
Vue.use(PortalVue) Vue.use(PortalVue)
Vue.use(VTooltip)
const i18n = new VueI18n({ const i18n = new VueI18n({
// By default, use the browser locale, we will update it if neccessary // By default, use the browser locale, we will update it if neccessary

View file

@ -492,10 +492,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]
@ -581,9 +590,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`
@ -65,6 +67,8 @@ const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials'
const MASTODON_REPORT_USER_URL = '/api/v1/reports' const MASTODON_REPORT_USER_URL = '/api/v1/reports'
const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin` 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_SEARCH_2 = `/api/v2/search`
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
const oldfetch = window.fetch const oldfetch = window.fetch
@ -76,7 +80,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: {
@ -85,6 +89,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)
} }
@ -746,6 +755,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))
@ -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 = { const apiService = {
verifyCredentials, verifyCredentials,
fetchTimeline, fetchTimeline,
@ -864,6 +935,8 @@ const apiService = {
fetchMutes, fetchMutes,
muteUser, muteUser,
unmuteUser, unmuteUser,
subscribeUser,
unsubscribeUser,
fetchBlocks, fetchBlocks,
fetchOAuthTokens, fetchOAuthTokens,
revokeOAuthToken, revokeOAuthToken,
@ -899,7 +972,9 @@ const apiService = {
fetchFavoritedByUsers, fetchFavoritedByUsers,
fetchRebloggedByUsers, fetchRebloggedByUsers,
reportUser, reportUser,
updateNotificationSettings updateNotificationSettings,
search2,
searchUsers
} }
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 })
@ -146,6 +148,9 @@ 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 searchUsers = (query) => apiService.searchUsers({ query, credentials })
const backendInteractorServiceInstance = { const backendInteractorServiceInstance = {
fetchStatus, fetchStatus,
@ -165,6 +170,8 @@ const backendInteractorService = credentials => {
fetchMutes, fetchMutes,
muteUser, muteUser,
unmuteUser, unmuteUser,
subscribeUser,
unsubscribeUser,
fetchBlocks, fetchBlocks,
fetchOAuthTokens, fetchOAuthTokens,
revokeOAuthToken, revokeOAuthToken,
@ -205,7 +212,9 @@ const backendInteractorService = credentials => {
unfavorite, unfavorite,
retweet, retweet,
unretweet, unretweet,
updateNotificationSettings updateNotificationSettings,
search2,
searchUsers
} }
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.

View file

@ -5459,9 +5459,10 @@ pngjs@^3.3.0:
version "3.3.3" version "3.3.3"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b"
popper.js@^1.14.7: popper.js@^1.15.0:
version "1.14.7" version "1.15.0"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e" 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: portal-vue@^2.1.4:
version "2.1.4" version "2.1.4"
@ -7198,6 +7199,15 @@ v-click-outside@^2.1.1:
version "2.1.3" version "2.1.3"
resolved "https://registry.yarnpkg.com/v-click-outside/-/v-click-outside-2.1.3.tgz#b7297abe833a439dc0895e6418a494381e64b5e7" 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: validate-npm-package-license@^3.0.1:
version "3.0.4" version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"
@ -7272,11 +7282,10 @@ vue-loader@^14.0.0:
vue-style-loader "^4.0.1" vue-style-loader "^4.0.1"
vue-template-es2015-compiler "^1.6.0" vue-template-es2015-compiler "^1.6.0"
vue-popperjs@^2.0.3: vue-resize@^0.4.5:
version "2.0.3" version "0.4.5"
resolved "https://registry.yarnpkg.com/vue-popperjs/-/vue-popperjs-2.0.3.tgz#7c446d0ba7c63170ccb33a02669d0df4efc3d8cd" resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-0.4.5.tgz#4777a23042e3c05620d9cbda01c0b3cc5e32dcea"
dependencies: integrity sha512-bhP7MlgJQ8TIkZJXAfDf78uJO+mEI3CaLABLjv0WNzr4CcGRGPIAItyWYnP6LsPA4Oq0WE+suidNs6dgpO4RHg==
popper.js "^1.14.7"
vue-router@^3.0.1: vue-router@^3.0.1:
version "3.0.2" version "3.0.2"