diff --git a/build/dev-server.js b/build/dev-server.js index 59dd2c4d39..4857421471 100644 --- a/build/dev-server.js +++ b/build/dev-server.js @@ -24,9 +24,6 @@ var devMiddleware = require('webpack-dev-middleware')(compiler, { stats: { colors: true, chunks: false - }, - headers: { - 'content-security-policy': "base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; script-src 'self' 'unsafe-eval';" } }) diff --git a/package.json b/package.json index 25bc419c28..28f3beba8d 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/boot/after_store.js b/src/boot/after_store.js index 3fcbc2467c..3799359f36 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -148,6 +148,37 @@ const getInstancePanel = async ({ store }) => { } } +const getStickers = async ({ store }) => { + try { + const res = await window.fetch('/static/stickers.json') + if (res.ok) { + const values = await res.json() + const stickers = (await Promise.all( + Object.entries(values).map(async ([name, path]) => { + const resPack = await window.fetch(path + 'pack.json') + var meta = {} + if (resPack.ok) { + meta = await resPack.json() + } + return { + pack: name, + path, + meta + } + }) + )).sort((a, b) => { + return a.meta.title.localeCompare(b.meta.title) + }) + store.dispatch('setInstanceOption', { name: 'stickers', value: stickers }) + } else { + throw (res) + } + } catch (e) { + console.warn("Can't load stickers") + console.warn(e) + } +} + const getStaticEmoji = async ({ store }) => { try { const res = await window.fetch('/static/emoji.json') @@ -286,6 +317,7 @@ const afterStoreSetup = async ({ store, i18n }) => { setConfig({ store }), getTOS({ store }), getInstancePanel({ store }), + getStickers({ store }), getStaticEmoji({ store }), getCustomEmoji({ store }), getNodeInfo({ store }) diff --git a/src/boot/routes.js b/src/boot/routes.js index 22641f833b..7dc4b2a5da 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -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: '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: 'user-profile', path: '/(users/)?:name', component: UserProfile } ] diff --git a/src/components/emoji-input/suggestor.js b/src/components/emoji-input/suggestor.js index a7ac203ee0..aec5c39d83 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 56b2c41ef8..8d1232938c 100644 --- a/src/components/extra_buttons/extra_buttons.js +++ b/src/components/extra_buttons/extra_buttons.js @@ -1,35 +1,18 @@ -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)) @@ -45,13 +28,6 @@ const ExtraButtons = { this.$store.dispatch('unmuteConversation', 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 5027be1be1..fc8000728e 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -1,18 +1,13 @@ @@ -73,7 +64,8 @@ .icon-ellipsis { cursor: pointer; - &:hover, &.icon-clicked { + &:hover, + .extra-button-popover.open & { color: $fallback--text; color: var(--text, $fallback--text); } diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js index 11de9f93e3..8aadc8c503 100644 --- a/src/components/moderation_tools/moderation_tools.js +++ b/src/components/moderation_tools/moderation_tools.js @@ -1,5 +1,4 @@ import DialogModal from '../dialog_modal/dialog_modal.vue' -import Popper from 'vue-popperjs/src/component/popper.js.vue' const FORCE_NSFW = 'mrf_tag:media-force-nsfw' const STRIP_MEDIA = 'mrf_tag:media-strip' @@ -29,8 +28,7 @@ const ModerationTools = { } }, components: { - DialogModal, - Popper + DialogModal }, computed: { tagsSet () { @@ -41,9 +39,6 @@ const ModerationTools = { } }, methods: { - toggleMenu () { - this.showDropDown = !this.showDropDown - }, hasTag (tagName) { return this.tagsSet.has(tagName) }, diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue index f1ab67a6ed..d97ca3aa6d 100644 --- a/src/components/moderation_tools/moderation_tools.vue +++ b/src/components/moderation_tools/moderation_tools.vue @@ -1,18 +1,15 @@ @@ -310,7 +325,7 @@ } } - .poll-icon { + .poll-icon, .sticker-icon { font-size: 26px; flex: 1; @@ -320,6 +335,11 @@ } } + .sticker-icon { + flex: 0; + min-width: 50px; + } + .icon-chart-bar { cursor: pointer; } diff --git a/src/components/status/status.js b/src/components/status/status.js index 7911d40d77..3c172e5b3c 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -110,8 +110,9 @@ const Status = { }, muteWordHits () { const statusText = this.status.text.toLowerCase() + const statusSummary = this.status.summary.toLowerCase() const hits = filter(this.muteWords, (muteWord) => { - return statusText.includes(muteWord.toLowerCase()) + return statusText.includes(muteWord.toLowerCase()) || statusSummary.includes(muteWord.toLowerCase()) }) return hits @@ -280,6 +281,11 @@ const Status = { }, tags () { return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ') + }, + hidePostStats () { + return typeof this.$store.state.config.hidePostStats === 'undefined' + ? this.$store.state.instance.hidePostStats + : this.$store.state.config.hidePostStats } }, components: { @@ -316,11 +322,8 @@ const Status = { this.error = undefined }, linkClicked (event) { - let { target } = event - if (target.tagName === 'SPAN') { - target = target.parentNode - } - if (target.tagName === 'A') { + const target = event.target.closest('.status-content a') + if (target) { if (target.className.match(/mention/)) { const href = target.href const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href)) diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 30969256f1..ab506632bc 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -344,7 +344,7 @@
@@ -820,11 +820,12 @@ $status-margin: 0.75em; } .status-actions { + position: relative; width: 100%; display: flex; margin-top: $status-margin; - div, favorite-button { + > * { max-width: 4em; flex: 1; } diff --git a/src/components/sticker_picker/sticker_picker.js b/src/components/sticker_picker/sticker_picker.js new file mode 100644 index 0000000000..a6dcded3cc --- /dev/null +++ b/src/components/sticker_picker/sticker_picker.js @@ -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 diff --git a/src/components/sticker_picker/sticker_picker.vue b/src/components/sticker_picker/sticker_picker.vue new file mode 100644 index 0000000000..938204c84f --- /dev/null +++ b/src/components/sticker_picker/sticker_picker.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js index 67835231b2..a5fe019c0e 100644 --- a/src/components/tab_switcher/tab_switcher.js +++ b/src/components/tab_switcher/tab_switcher.js @@ -45,7 +45,19 @@ export default Vue.component('tab-switcher', { classesTab.push('active') classesWrapper.push('active') } - + if (slot.data.attrs.image) { + return ( +
+ +
+ ) + } return (