diff --git a/package.json b/package.json
index 25bc419c2..28f3beba8 100644
--- a/package.json
+++ b/package.json
@@ -25,14 +25,13 @@
"localforage": "^1.5.0",
"object-path": "^0.11.3",
"phoenix": "^1.3.0",
- "popper.js": "^1.14.7",
"portal-vue": "^2.1.4",
"sanitize-html": "^1.13.0",
"v-click-outside": "^2.1.1",
+ "v-tooltip": "^2.0.2",
"vue": "^2.5.13",
"vue-chat-scroll": "^1.2.1",
"vue-i18n": "^7.3.2",
- "vue-popperjs": "^2.0.3",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.3.4",
"vuelidate": "^0.7.4",
@@ -81,8 +80,8 @@
"json-loader": "^0.5.4",
"karma": "^3.0.0",
"karma-coverage": "^1.1.1",
- "karma-mocha": "^1.2.0",
"karma-firefox-launcher": "^1.1.0",
+ "karma-mocha": "^1.2.0",
"karma-sinon-chai": "^2.0.2",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.26",
diff --git a/src/App.js b/src/App.js
index e72c73e35..3624171ed 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,7 +1,7 @@
import UserPanel from './components/user_panel/user_panel.vue'
import NavPanel from './components/nav_panel/nav_panel.vue'
import Notifications from './components/notifications/notifications.vue'
-import UserFinder from './components/user_finder/user_finder.vue'
+import SearchBar from './components/search_bar/search_bar.vue'
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
@@ -19,7 +19,7 @@ export default {
UserPanel,
NavPanel,
Notifications,
- UserFinder,
+ SearchBar,
InstanceSpecificPanel,
FeaturesPanel,
WhoToFollowPanel,
@@ -32,7 +32,7 @@ export default {
},
data: () => ({
mobileActivePanel: 'timeline',
- finderHidden: true,
+ searchBarHidden: true,
supportsMask: window.CSS && window.CSS.supports && (
window.CSS.supports('mask-size', 'contain') ||
window.CSS.supports('-webkit-mask-size', 'contain') ||
@@ -70,7 +70,7 @@ export default {
logoBgStyle () {
return Object.assign({
'margin': `${this.$store.state.instance.logoMargin} 0`,
- opacity: this.finderHidden ? 1 : 0
+ opacity: this.searchBarHidden ? 1 : 0
}, this.enableMask ? {} : {
'background-color': this.enableMask ? '' : 'transparent'
})
@@ -101,8 +101,8 @@ export default {
this.$router.replace('/main/public')
this.$store.dispatch('logout')
},
- onFinderToggled (hidden) {
- this.finderHidden = hidden
+ onSearchBarToggled (hidden) {
+ this.searchBarHidden = hidden
},
updateMobileState () {
const mobileLayout = windowWidth() <= 800
diff --git a/src/App.scss b/src/App.scss
index e4c764bf0..1299e05dd 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -283,6 +283,31 @@ i[class*=icon-] {
color: var(--icon, $fallback--icon)
}
+.btn-block {
+ display: block;
+ width: 100%;
+}
+
+.btn-group {
+ position: relative;
+ display: inline-flex;
+ vertical-align: middle;
+
+ button {
+ position: relative;
+ flex: 1 1 auto;
+
+ &:not(:last-child) {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+
+ &:not(:first-child) {
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+ }
+}
.container {
display: flex;
diff --git a/src/App.vue b/src/App.vue
index 758c9fce1..be4d1f754 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -38,9 +38,9 @@
-
{
}
}
+const getStickers = async ({ store }) => {
+ try {
+ const res = await window.fetch('/static/stickers.json')
+ if (res.ok) {
+ const values = await res.json()
+ const stickers = (await Promise.all(
+ Object.entries(values).map(async ([name, path]) => {
+ const resPack = await window.fetch(path + 'pack.json')
+ var meta = {}
+ if (resPack.ok) {
+ meta = await resPack.json()
+ }
+ return {
+ pack: name,
+ path,
+ meta
+ }
+ })
+ )).sort((a, b) => {
+ return a.meta.title.localeCompare(b.meta.title)
+ })
+ store.dispatch('setInstanceOption', { name: 'stickers', value: stickers })
+ } else {
+ throw (res)
+ }
+ } catch (e) {
+ console.warn("Can't load stickers")
+ console.warn(e)
+ }
+}
+
const getStaticEmoji = async ({ store }) => {
try {
const res = await window.fetch('/static/emoji.json')
@@ -286,6 +317,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
setConfig({ store }),
getTOS({ store }),
getInstancePanel({ store }),
+ getStickers({ store }),
getStaticEmoji({ store }),
getCustomEmoji({ store }),
getNodeInfo({ store })
diff --git a/src/boot/routes.js b/src/boot/routes.js
index ca4a6a3ee..7dc4b2a5d 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -6,12 +6,12 @@ import ConversationPage from 'components/conversation-page/conversation-page.vue
import Interactions from 'components/interactions/interactions.vue'
import DMs from 'components/dm_timeline/dm_timeline.vue'
import UserProfile from 'components/user_profile/user_profile.vue'
+import Search from 'components/search/search.vue'
import Settings from 'components/settings/settings.vue'
import Registration from 'components/registration/registration.vue'
import UserSettings from 'components/user_settings/user_settings.vue'
import FollowRequests from 'components/follow_requests/follow_requests.vue'
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
-import UserSearch from 'components/user_search/user_search.vue'
import Notifications from 'components/notifications/notifications.vue'
import AuthForm from 'components/auth_form/auth_form.js'
import ChatPanel from 'components/chat_panel/chat_panel.vue'
@@ -19,6 +19,14 @@ import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
import About from 'components/about/about.vue'
export default (store) => {
+ const validateAuthenticatedRoute = (to, from, next) => {
+ if (store.state.users.currentUser) {
+ next()
+ } else {
+ next(store.state.instance.redirectRootNoLogin || '/main/all')
+ }
+ }
+
return [
{ name: 'root',
path: '/',
@@ -30,23 +38,23 @@ export default (store) => {
},
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
- { name: 'friends', path: '/main/friends', component: FriendsTimeline },
+ { name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
{ name: 'external-user-profile', path: '/users/:id', component: UserProfile },
- { name: 'interactions', path: '/users/:username/interactions', component: Interactions },
- { name: 'dms', path: '/users/:username/dms', component: DMs },
+ { name: 'interactions', path: '/users/:username/interactions', component: Interactions, beforeEnter: validateAuthenticatedRoute },
+ { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration },
{ name: 'registration-token', path: '/registration/:token', component: Registration },
- { name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
- { name: 'user-settings', path: '/user-settings', component: UserSettings },
- { name: 'notifications', path: '/:username/notifications', component: Notifications },
+ { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
+ { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },
+ { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute },
{ name: 'login', path: '/login', component: AuthForm },
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
- { name: 'user-search', path: '/user-search', component: UserSearch, props: (route) => ({ query: route.query.query }) },
- { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow },
+ { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
+ { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
{ name: 'about', path: '/about', component: About },
{ name: 'user-profile', path: '/(users/)?:name', component: UserProfile }
]
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 108dc36e7..ec326c45d 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -100,7 +100,7 @@
-
+
diff --git a/src/components/emoji-input/suggestor.js b/src/components/emoji-input/suggestor.js
index a7ac203ee..aec5c39d8 100644
--- a/src/components/emoji-input/suggestor.js
+++ b/src/components/emoji-input/suggestor.js
@@ -1,20 +1,27 @@
+import { debounce } from 'lodash'
/**
* suggest - generates a suggestor function to be used by emoji-input
* data: object providing source information for specific types of suggestions:
* data.emoji - optional, an array of all emoji available i.e.
* (state.instance.emoji + state.instance.customEmoji)
* data.users - optional, an array of all known users
+ * updateUsersList - optional, a function to search and append to users
*
* Depending on data present one or both (or none) can be present, so if field
* doesn't support user linking you can just provide only emoji.
*/
+
+const debounceUserSearch = debounce((data, input) => {
+ data.updateUsersList(input)
+}, 500, { leading: true, trailing: false })
+
export default data => input => {
const firstChar = input[0]
if (firstChar === ':' && data.emoji) {
return suggestEmoji(data.emoji)(input)
}
if (firstChar === '@' && data.users) {
- return suggestUsers(data.users)(input)
+ return suggestUsers(data)(input)
}
return []
}
@@ -38,9 +45,11 @@ export const suggestEmoji = emojis => input => {
})
}
-export const suggestUsers = users => input => {
+export const suggestUsers = data => input => {
const noPrefix = input.toLowerCase().substr(1)
- return users.filter(
+ const users = data.users
+
+ const newUsers = users.filter(
user =>
user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix)
@@ -75,5 +84,11 @@ export const suggestUsers = users => input => {
imageUrl: profile_image_url_original,
replacement: '@' + screen_name + ' '
}))
+
+ // BE search users if there are no matches
+ if (newUsers.length === 0 && data.updateUsersList) {
+ debounceUserSearch(data, noPrefix)
+ }
+ return newUsers
/* eslint-enable camelcase */
}
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index 528da301f..2ec727294 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -1,45 +1,21 @@
-import Popper from 'vue-popperjs/src/component/popper.js.vue'
-
const ExtraButtons = {
props: [ 'status' ],
- components: {
- Popper
- },
- data () {
- return {
- showDropDown: false,
- showPopper: true
- }
- },
methods: {
deleteStatus () {
- this.refreshPopper()
const confirmed = window.confirm(this.$t('status.delete_confirm'))
if (confirmed) {
this.$store.dispatch('deleteStatus', { id: this.status.id })
}
},
- toggleMenu () {
- this.showDropDown = !this.showDropDown
- },
pinStatus () {
- this.refreshPopper()
this.$store.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
},
unpinStatus () {
- this.refreshPopper()
this.$store.dispatch('unpinStatus', this.status.id)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
- },
- refreshPopper () {
- this.showPopper = false
- this.showDropDown = false
- setTimeout(() => {
- this.showPopper = true
- })
}
},
computed: {
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index 8e24e9a53..cdad16667 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -1,21 +1,17 @@
-