diff --git a/src/App.js b/src/App.js index 214e0f48..5c27a3df 100644 --- a/src/App.js +++ b/src/App.js @@ -8,6 +8,7 @@ import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_pan import ChatPanel from './components/chat_panel/chat_panel.vue' import MediaModal from './components/media_modal/media_modal.vue' import SideDrawer from './components/side_drawer/side_drawer.vue' +import MobilePostStatusModal from './components/mobile_post_status_modal/mobile_post_status_modal.vue' import { unseenNotificationsFromStore } from './services/notification_utils/notification_utils' export default { @@ -22,7 +23,8 @@ export default { WhoToFollowPanel, ChatPanel, MediaModal, - SideDrawer + SideDrawer, + MobilePostStatusModal }, data: () => ({ mobileActivePanel: 'timeline', diff --git a/src/App.scss b/src/App.scss index a0d1a804..244b3474 100644 --- a/src/App.scss +++ b/src/App.scss @@ -154,7 +154,7 @@ input, textarea, .select { background: transparent; border: none; color: $fallback--text; - color: var(--text, $fallback--text); + color: var(--inputText, --text, $fallback--text); margin: 0; padding: 0 2em 0 .2em; font-family: sans-serif; @@ -671,6 +671,31 @@ nav { border-radius: var(--inputRadius, $fallback--inputRadius); } +@keyframes modal-background-fadein { + from { + background-color: rgba(0, 0, 0, 0); + } + to { + background-color: rgba(0, 0, 0, 0.5); + } +} + +.modal-view { + z-index: 1000; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + overflow: auto; + animation-duration: 0.2s; + background-color: rgba(0, 0, 0, 0.5); + animation-name: modal-background-fadein; +} + .button-icon { font-size: 1.2em; } diff --git a/src/App.vue b/src/App.vue index acbbeb75..4fff3d1d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -50,6 +50,7 @@ <media-modal></media-modal> </div> <chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel> + <MobilePostStatusModal /> </div> </template> diff --git a/src/boot/after_store.js b/src/boot/after_store.js index a8e2bf35..a5f8c978 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -4,10 +4,11 @@ import routes from './routes' import App from '../App.vue' -const afterStoreSetup = ({ store, i18n }) => { - window.fetch('/api/statusnet/config.json') - .then((res) => res.json()) - .then((data) => { +const getStatusnetConfig = async ({ store }) => { + try { + const res = await window.fetch('/api/statusnet/config.json') + if (res.ok) { + const data = await res.json() const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey } = data.site store.dispatch('setInstanceOption', { name: 'name', value: name }) @@ -28,140 +29,167 @@ const afterStoreSetup = ({ store, i18n }) => { store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) } - var apiConfig = data.site.pleromafe + return data.site.pleromafe + } else { + throw (res) + } + } catch (error) { + console.error('Could not load statusnet config, potentially fatal') + console.error(error) + } +} - window.fetch('/static/config.json') - .then((res) => res.json()) - .catch((err) => { - console.warn('Failed to load static/config.json, continuing without it.') - console.warn(err) - return {} - }) - .then((staticConfig) => { - const overrides = window.___pleromafe_dev_overrides || {} - const env = window.___pleromafe_mode.NODE_ENV +const getStaticConfig = async () => { + try { + const res = await window.fetch('/static/config.json') + if (res.ok) { + return res.json() + } else { + throw (res) + } + } catch (error) { + console.warn('Failed to load static/config.json, continuing without it.') + console.warn(error) + return {} + } +} - // This takes static config and overrides properties that are present in apiConfig - let config = {} - if (overrides.staticConfigPreference && env === 'development') { - console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG') - config = Object.assign({}, apiConfig, staticConfig) - } else { - config = Object.assign({}, staticConfig, apiConfig) - } +const setSettings = async ({ apiConfig, staticConfig, store }) => { + const overrides = window.___pleromafe_dev_overrides || {} + const env = window.___pleromafe_mode.NODE_ENV - const copyInstanceOption = (name) => { - store.dispatch('setInstanceOption', {name, value: config[name]}) - } + // This takes static config and overrides properties that are present in apiConfig + let config = {} + if (overrides.staticConfigPreference && env === 'development') { + console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG') + config = Object.assign({}, apiConfig, staticConfig) + } else { + config = Object.assign({}, staticConfig, apiConfig) + } - copyInstanceOption('nsfwCensorImage') - copyInstanceOption('background') - copyInstanceOption('hidePostStats') - copyInstanceOption('hideUserStats') - copyInstanceOption('hideFilteredStatuses') - copyInstanceOption('logo') + const copyInstanceOption = (name) => { + store.dispatch('setInstanceOption', { name, value: config[name] }) + } - store.dispatch('setInstanceOption', { - name: 'logoMask', - value: typeof config.logoMask === 'undefined' - ? true - : config.logoMask - }) + copyInstanceOption('nsfwCensorImage') + copyInstanceOption('background') + copyInstanceOption('hidePostStats') + copyInstanceOption('hideUserStats') + copyInstanceOption('hideFilteredStatuses') + copyInstanceOption('logo') - store.dispatch('setInstanceOption', { - name: 'logoMargin', - value: typeof config.logoMargin === 'undefined' - ? 0 - : config.logoMargin - }) + store.dispatch('setInstanceOption', { + name: 'logoMask', + value: typeof config.logoMask === 'undefined' + ? true + : config.logoMask + }) - copyInstanceOption('redirectRootNoLogin') - copyInstanceOption('redirectRootLogin') - copyInstanceOption('showInstanceSpecificPanel') - copyInstanceOption('scopeOptionsEnabled') - copyInstanceOption('formattingOptionsEnabled') - copyInstanceOption('collapseMessageWithSubject') - copyInstanceOption('loginMethod') - copyInstanceOption('scopeCopy') - copyInstanceOption('subjectLineBehavior') - copyInstanceOption('postContentType') - copyInstanceOption('alwaysShowSubjectInput') - copyInstanceOption('noAttachmentLinks') - copyInstanceOption('showFeaturesPanel') + store.dispatch('setInstanceOption', { + name: 'logoMargin', + value: typeof config.logoMargin === 'undefined' + ? 0 + : config.logoMargin + }) - if ((config.chatDisabled)) { - store.dispatch('disableChat') - } else { - store.dispatch('initializeSocket') - } + copyInstanceOption('redirectRootNoLogin') + copyInstanceOption('redirectRootLogin') + copyInstanceOption('showInstanceSpecificPanel') + copyInstanceOption('scopeOptionsEnabled') + copyInstanceOption('formattingOptionsEnabled') + copyInstanceOption('collapseMessageWithSubject') + copyInstanceOption('loginMethod') + copyInstanceOption('scopeCopy') + copyInstanceOption('subjectLineBehavior') + copyInstanceOption('postContentType') + copyInstanceOption('alwaysShowSubjectInput') + copyInstanceOption('noAttachmentLinks') + copyInstanceOption('showFeaturesPanel') - return store.dispatch('setTheme', config['theme']) - }) - .then(() => { - const router = new VueRouter({ - mode: 'history', - routes: routes(store), - scrollBehavior: (to, _from, savedPosition) => { - if (to.matched.some(m => m.meta.dontScroll)) { - return false - } - return savedPosition || { x: 0, y: 0 } - } - }) + if ((config.chatDisabled)) { + store.dispatch('disableChat') + } else { + store.dispatch('initializeSocket') + } - /* eslint-disable no-new */ - new Vue({ - router, - store, - i18n, - el: '#app', - render: h => h(App) - }) - }) - }) + return store.dispatch('setTheme', config['theme']) +} - window.fetch('/static/terms-of-service.html') - .then((res) => res.text()) - .then((html) => { +const getTOS = async ({ store }) => { + try { + const res = await window.fetch('/static/terms-of-service.html') + if (res.ok) { + const html = await res.text() store.dispatch('setInstanceOption', { name: 'tos', value: html }) - }) + } else { + throw (res) + } + } catch (e) { + console.warn("Can't load TOS") + console.warn(e) + } +} - window.fetch('/api/pleroma/emoji.json') - .then( - (res) => res.json() - .then( - (values) => { - const emoji = Object.keys(values).map((key) => { - return { shortcode: key, image_url: values[key] } - }) - store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji }) - store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true }) - }, - (failure) => { - store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false }) - } - ), - (error) => console.log(error) - ) +const getInstancePanel = async ({ store }) => { + try { + const res = await window.fetch('/instance/panel.html') + if (res.ok) { + const html = await res.text() + store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html }) + } else { + throw (res) + } + } catch (e) { + console.warn("Can't load instance panel") + console.warn(e) + } +} - window.fetch('/static/emoji.json') - .then((res) => res.json()) - .then((values) => { +const getStaticEmoji = async ({ store }) => { + try { + const res = await window.fetch('/static/emoji.json') + if (res.ok) { + const values = await res.json() const emoji = Object.keys(values).map((key) => { return { shortcode: key, image_url: false, 'utf': values[key] } }) store.dispatch('setInstanceOption', { name: 'emoji', value: emoji }) - }) + } else { + throw (res) + } + } catch (e) { + console.warn("Can't load static emoji") + console.warn(e) + } +} - window.fetch('/instance/panel.html') - .then((res) => res.text()) - .then((html) => { - store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html }) - }) +// This is also used to indicate if we have a 'pleroma backend' or not. +// Somewhat weird, should probably be somewhere else. +const getCustomEmoji = async ({ store }) => { + try { + const res = await window.fetch('/api/pleroma/emoji.json') + if (res.ok) { + const values = await res.json() + const emoji = Object.keys(values).map((key) => { + return { shortcode: key, image_url: values[key] } + }) + store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji }) + store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true }) + } else { + throw (res) + } + } catch (e) { + store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false }) + console.warn("Can't load custom emojis, maybe not a Pleroma instance?") + console.warn(e) + } +} - window.fetch('/nodeinfo/2.0.json') - .then((res) => res.json()) - .then((data) => { +const getNodeInfo = async ({ store }) => { + try { + const res = await window.fetch('/nodeinfo/2.0.json') + if (res.ok) { + const data = await res.json() const metadata = data.metadata const features = metadata.features @@ -169,14 +197,71 @@ const afterStoreSetup = ({ store, i18n }) => { store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') }) store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) - store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats }) - store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames }) + store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats }) const suggestions = metadata.suggestions store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled }) store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web }) + + const software = data.software + store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version }) + + const frontendVersion = window.___pleromafe_commit_hash + store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion }) + } else { + throw (res) + } + } catch (e) { + console.warn('Could not load nodeinfo') + console.warn(e) + } +} + +const afterStoreSetup = async ({ store, i18n }) => { + if (store.state.config.customTheme) { + // This is a hack to deal with async loading of config.json and themes + // See: style_setter.js, setPreset() + window.themeLoaded = true + store.dispatch('setOption', { + name: 'customTheme', + value: store.state.config.customTheme }) + } + + const apiConfig = await getStatusnetConfig({ store }) + const staticConfig = await getStaticConfig() + await setSettings({ store, apiConfig, staticConfig }) + await getTOS({ store }) + await getInstancePanel({ store }) + await getStaticEmoji({ store }) + await getCustomEmoji({ store }) + await getNodeInfo({ store }) + + // Now we have the server settings and can try logging in + if (store.state.oauth.token) { + store.dispatch('loginUser', store.state.oauth.token) + } + + const router = new VueRouter({ + mode: 'history', + routes: routes(store), + scrollBehavior: (to, _from, savedPosition) => { + if (to.matched.some(m => m.meta.dontScroll)) { + return false + } + return savedPosition || { x: 0, y: 0 } + } + }) + + /* eslint-disable no-new */ + return new Vue({ + router, + store, + i18n, + el: '#app', + render: h => h(App) + }) } export default afterStoreSetup diff --git a/src/boot/routes.js b/src/boot/routes.js index cfbcb1fe..7e54a98b 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -13,7 +13,6 @@ 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 UserPanel from 'components/user_panel/user_panel.vue' import LoginForm from 'components/login_form/login_form.vue' import ChatPanel from 'components/chat_panel/chat_panel.vue' import WhoToFollow from 'components/who_to_follow/who_to_follow.vue' @@ -43,7 +42,6 @@ export default (store) => { { name: 'friend-requests', path: '/friend-requests', component: FollowRequests }, { name: 'user-settings', path: '/user-settings', component: UserSettings }, { name: 'notifications', path: '/:username/notifications', component: Notifications }, - { name: 'new-status', path: '/:username/new-status', component: UserPanel }, { name: 'login', path: '/login', component: LoginForm }, { name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) }, { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, diff --git a/src/components/block_card/block_card.js b/src/components/block_card/block_card.js index 11fa27b4..c459ff1b 100644 --- a/src/components/block_card/block_card.js +++ b/src/components/block_card/block_card.js @@ -9,7 +9,7 @@ const BlockCard = { }, computed: { user () { - return this.$store.getters.userById(this.userId) + return this.$store.getters.findUser(this.userId) }, blocked () { return this.user.statusnet_blocking diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js index 425c9c3e..ac4e265a 100644 --- a/src/components/follow_card/follow_card.js +++ b/src/components/follow_card/follow_card.js @@ -1,4 +1,5 @@ import BasicUserCard from '../basic_user_card/basic_user_card.vue' +import RemoteFollow from '../remote_follow/remote_follow.vue' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' const FollowCard = { @@ -14,13 +15,17 @@ const FollowCard = { } }, components: { - BasicUserCard + BasicUserCard, + RemoteFollow }, computed: { isMe () { return this.$store.state.users.currentUser.id === this.user.id }, following () { return this.updated ? this.updated.following : this.user.following }, showFollow () { return !this.following || this.updated && !this.updated.following + }, + loggedIn () { + return this.$store.state.users.currentUser } }, methods: { diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue index 6cb064eb..9bd21cfd 100644 --- a/src/components/follow_card/follow_card.vue +++ b/src/components/follow_card/follow_card.vue @@ -4,9 +4,12 @@ <span class="faint" v-if="!noFollowsYou && user.follows_you"> {{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }} </span> + <div class="btn-follow" v-if="showFollow && !loggedIn"> + <RemoteFollow :user="user" /> + </div> <button - v-if="showFollow" - class="btn btn-default" + v-if="showFollow && loggedIn" + class="btn btn-default btn-follow" @click="followUser" :disabled="inProgress" :title="requestSent ? $t('user_card.follow_again') : ''" @@ -44,7 +47,7 @@ flex-wrap: wrap; line-height: 1.5em; - .btn { + .btn-follow { margin-top: 0.5em; margin-left: auto; width: 10em; diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js index 49d51846..5ba8f04e 100644 --- a/src/components/image_cropper/image_cropper.js +++ b/src/components/image_cropper/image_cropper.js @@ -31,6 +31,9 @@ const ImageCropper = { saveButtonLabel: { type: String }, + saveWithoutCroppingButtonlabel: { + type: String + }, cancelButtonLabel: { type: String } @@ -48,6 +51,9 @@ const ImageCropper = { saveText () { return this.saveButtonLabel || this.$t('image_cropper.save') }, + saveWithoutCroppingText () { + return this.saveWithoutCroppingButtonlabel || this.$t('image_cropper.save_without_cropping') + }, cancelText () { return this.cancelButtonLabel || this.$t('image_cropper.cancel') }, @@ -76,6 +82,18 @@ const ImageCropper = { this.submitting = false }) }, + submitWithoutCropping () { + this.submitting = true + this.avatarUploadError = null + this.submitHandler(false, this.dataUrl) + .then(() => this.destroy()) + .catch((err) => { + this.submitError = err + }) + .finally(() => { + this.submitting = false + }) + }, pickImage () { this.$refs.input.click() }, diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue index 24a6f3bd..129e6f46 100644 --- a/src/components/image_cropper/image_cropper.vue +++ b/src/components/image_cropper/image_cropper.vue @@ -7,6 +7,7 @@ <div class="image-cropper-buttons-wrapper"> <button class="btn" type="button" :disabled="submitting" @click="submit" v-text="saveText"></button> <button class="btn" type="button" :disabled="submitting" @click="destroy" v-text="cancelText"></button> + <button class="btn" type="button" :disabled="submitting" @click="submitWithoutCropping" v-text="saveWithoutCroppingText"></button> <i class="icon-spin4 animate-spin" v-if="submitting"></i> </div> <div class="alert error" v-if="submitError"> @@ -36,7 +37,11 @@ } &-buttons-wrapper { - margin-top: 15px; + margin-top: 10px; + + button { + margin-top: 5px; + } } } </style> diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 427bf12b..7f666603 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -1,5 +1,5 @@ <template> - <div class="modal-view" v-if="showing" @click.prevent="hide"> + <div class="modal-view media-modal-view" v-if="showing" @click.prevent="hide"> <img class="modal-image" v-if="type === 'image'" :src="currentMedia.url"></img> <VideoAttachment class="modal-image" @@ -32,18 +32,7 @@ <style lang="scss"> @import '../../_variables.scss'; -.modal-view { - z-index: 1000; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: flex; - justify-content: center; - align-items: center; - background-color: rgba(0, 0, 0, 0.5); - +.media-modal-view { &:hover { .modal-view-button-arrow { opacity: 0.75; diff --git a/src/components/mobile_post_status_modal/mobile_post_status_modal.js b/src/components/mobile_post_status_modal/mobile_post_status_modal.js new file mode 100644 index 00000000..2f24dd08 --- /dev/null +++ b/src/components/mobile_post_status_modal/mobile_post_status_modal.js @@ -0,0 +1,91 @@ +import PostStatusForm from '../post_status_form/post_status_form.vue' +import { throttle } from 'lodash' + +const MobilePostStatusModal = { + components: { + PostStatusForm + }, + data () { + return { + hidden: false, + postFormOpen: false, + scrollingDown: false, + inputActive: false, + oldScrollPos: 0, + amountScrolled: 0 + } + }, + created () { + window.addEventListener('scroll', this.handleScroll) + window.addEventListener('resize', this.handleOSK) + }, + destroyed () { + window.removeEventListener('scroll', this.handleScroll) + window.removeEventListener('resize', this.handleOSK) + }, + computed: { + currentUser () { + return this.$store.state.users.currentUser + }, + isHidden () { + return this.hidden || this.inputActive + } + }, + methods: { + openPostForm () { + this.postFormOpen = true + this.hidden = true + + const el = this.$el.querySelector('textarea') + this.$nextTick(function () { + el.focus() + }) + }, + closePostForm () { + this.postFormOpen = false + this.hidden = false + }, + handleOSK () { + // This is a big hack: we're guessing from changed window sizes if the + // on-screen keyboard is active or not. This is only really important + // for phones in portrait mode and it's more important to show the button + // in normal scenarios on all phones, than it is to hide it when the + // keyboard is active. + // Guesswork based on https://www.mydevice.io/#compare-devices + + // for example, iphone 4 and android phones from the same time period + const smallPhone = window.innerWidth < 350 + const smallPhoneKbOpen = smallPhone && window.innerHeight < 345 + + const biggerPhone = !smallPhone && window.innerWidth < 450 + const biggerPhoneKbOpen = biggerPhone && window.innerHeight < 560 + if (smallPhoneKbOpen || biggerPhoneKbOpen) { + this.inputActive = true + } else { + this.inputActive = false + } + }, + handleScroll: throttle(function () { + const scrollAmount = window.scrollY - this.oldScrollPos + const scrollingDown = scrollAmount > 0 + + if (scrollingDown !== this.scrollingDown) { + this.amountScrolled = 0 + this.scrollingDown = scrollingDown + if (!scrollingDown) { + this.hidden = false + } + } else if (scrollingDown) { + this.amountScrolled += scrollAmount + if (this.amountScrolled > 100 && !this.hidden) { + this.hidden = true + } + } + + this.oldScrollPos = window.scrollY + this.scrollingDown = scrollingDown + }, 100) + } +} + +export default MobilePostStatusModal diff --git a/src/components/mobile_post_status_modal/mobile_post_status_modal.vue b/src/components/mobile_post_status_modal/mobile_post_status_modal.vue new file mode 100644 index 00000000..0a451c28 --- /dev/null +++ b/src/components/mobile_post_status_modal/mobile_post_status_modal.vue @@ -0,0 +1,76 @@ +<template> +<div v-if="currentUser"> + <div + class="post-form-modal-view modal-view" + v-show="postFormOpen" + @click="closePostForm" + > + <div class="post-form-modal-panel panel" @click.stop=""> + <div class="panel-heading">{{$t('post_status.new_status')}}</div> + <PostStatusForm class="panel-body" @posted="closePostForm"/> + </div> + </div> + <button + class="new-status-button" + :class="{ 'hidden': isHidden }" + @click="openPostForm" + > + <i class="icon-edit" /> + </button> +</div> +</template> + +<script src="./mobile_post_status_modal.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.post-form-modal-view { + max-height: 100%; + display: block; +} + +.post-form-modal-panel { + flex-shrink: 0; + margin: 25% 0 4em 0; + width: 100%; +} + +.new-status-button { + width: 5em; + height: 5em; + border-radius: 100%; + position: fixed; + bottom: 1.5em; + right: 1.5em; + // TODO: this needs its own color, it has to stand out enough and link color + // is not very optimal for this particular use. + background-color: $fallback--fg; + background-color: var(--btn, $fallback--fg); + display: flex; + justify-content: center; + align-items: center; + box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.3); + z-index: 10; + + transition: 0.35s transform; + transition-timing-function: cubic-bezier(0, 1, 0.5, 1); + + &.hidden { + transform: translateY(150%); + } + + i { + font-size: 1.5em; + color: $fallback--text; + color: var(--text, $fallback--text); + } +} + +@media all and (min-width: 801px) { + .new-status-button { + display: none; + } +} + +</style> diff --git a/src/components/mute_card/mute_card.js b/src/components/mute_card/mute_card.js index 5dd0a9e5..65c9cfb5 100644 --- a/src/components/mute_card/mute_card.js +++ b/src/components/mute_card/mute_card.js @@ -9,7 +9,7 @@ const MuteCard = { }, computed: { user () { - return this.$store.getters.userById(this.userId) + return this.$store.getters.findUser(this.userId) }, muted () { return this.user.muted diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 23a2c7e2..1f0df35a 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -222,6 +222,9 @@ const PostStatusForm = { this.highlighted = 0 } }, + onKeydown (e) { + e.stopPropagation() + }, setCaret ({target: {selectionStart}}) { this.caret = selectionStart }, diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 0ddde4ea..3d1df91b 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -20,6 +20,7 @@ ref="textarea" @click="setCaret" @keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control" + @keydown="onKeydown" @keydown.down="cycleForward" @keydown.up="cycleBackward" @keydown.shift.tab="cycleBackward" diff --git a/src/components/remote_follow/remote_follow.js b/src/components/remote_follow/remote_follow.js new file mode 100644 index 00000000..461d58c9 --- /dev/null +++ b/src/components/remote_follow/remote_follow.js @@ -0,0 +1,10 @@ +export default { + props: [ 'user' ], + computed: { + subscribeUrl () { + // eslint-disable-next-line no-undef + const serverUrl = new URL(this.user.statusnet_profile_url) + return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus` + } + } +} diff --git a/src/components/remote_follow/remote_follow.vue b/src/components/remote_follow/remote_follow.vue new file mode 100644 index 00000000..fb2147bd --- /dev/null +++ b/src/components/remote_follow/remote_follow.vue @@ -0,0 +1,24 @@ +<template> + <div class="remote-follow"> + <form method="POST" :action='subscribeUrl'> + <input type="hidden" name="nickname" :value="user.screen_name"> + <input type="hidden" name="profile" value=""> + <button click="submit" class="remote-button"> + {{ $t('user_card.remote_follow') }} + </button> + </form> + </div> +</template> + +<script src="./remote_follow.js"></script> + +<style lang="scss"> +.remote-follow { + max-width: 220px; + + .remote-button { + width: 100%; + min-height: 28px; + } +} +</style> diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index 979457a5..b77c5197 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -1,8 +1,13 @@ /* eslint-env browser */ +import { filter, trim } from 'lodash' + import TabSwitcher from '../tab_switcher/tab_switcher.js' import StyleSwitcher from '../style_switcher/style_switcher.vue' import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue' -import { filter, trim } from 'lodash' +import { extractCommit } from '../../services/version/version.service' + +const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/' +const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/' const settings = { data () { @@ -78,7 +83,10 @@ const settings = { // Future spec, still not supported in Nightly 63 as of 08/2018 Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'), playVideosInModal: user.playVideosInModal, - useContainFit: user.useContainFit + useContainFit: user.useContainFit, + + backendVersion: instance.backendVersion, + frontendVersion: instance.frontendVersion } }, components: { @@ -96,7 +104,13 @@ const settings = { postFormats () { return this.$store.state.instance.postFormats || [] }, - instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel } + instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, + frontendVersionLink () { + return pleromaFeCommitUrl + this.frontendVersion + }, + backendVersionLink () { + return pleromaBeCommitUrl + extractCommit(this.backendVersion) + } }, watch: { hideAttachmentsLocal (value) { diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index d2346747..17f1f1a1 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -261,6 +261,28 @@ </div> </div> </div> + <div :label="$t('settings.version.title')" > + <div class="setting-item"> + <ul class="setting-list"> + <li> + <p>{{$t('settings.version.backend_version')}}</p> + <ul class="option-list"> + <li> + <a :href="backendVersionLink" target="_blank">{{backendVersion}}</a> + </li> + </ul> + </li> + <li> + <p>{{$t('settings.version.frontend_version')}}</p> + <ul class="option-list"> + <li> + <a :href="frontendVersionLink" target="_blank">{{frontendVersion}}</a> + </li> + </ul> + </li> + </ul> + </div> + </div> </tab-switcher> </keep-alive> </div> diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index b608b008..95ee21b4 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -15,12 +15,7 @@ </div> </div> <ul> - <li v-if="currentUser" @click="toggleDrawer"> - <router-link :to="{ name: 'new-status', params: { username: currentUser.screen_name } }"> - {{ $t("post_status.new_status") }} - </router-link> - </li> - <li v-else @click="toggleDrawer"> + <li v-if="!currentUser" @click="toggleDrawer"> <router-link :to="{ name: 'login' }"> {{ $t("login.login") }} </router-link> @@ -119,14 +114,14 @@ } .side-drawer-container-open { - transition-delay: 0.0s; - transition-property: left; + transition: 0.35s; + transition-property: background-color; + background-color: rgba(0, 0, 0, 0.5); } .side-drawer-container-closed { left: -100%; - transition-delay: 0.5s; - transition-property: left; + background-color: rgba(0, 0, 0, 0); } .side-drawer-click-outside { diff --git a/src/components/status/status.js b/src/components/status/status.js index 9e18fe15..c90da6d4 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -145,11 +145,11 @@ const Status = { return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id) }, replyToName () { - const user = this.$store.state.users.usersObject[this.status.in_reply_to_user_id] - if (user) { - return user.screen_name - } else { + if (this.status.in_reply_to_screen_name) { return this.status.in_reply_to_screen_name + } else { + const user = this.$store.getters.findUser(this.status.in_reply_to_user_id) + return user && user.screen_name } }, hideReply () { diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js index e513b993..e6fed3b5 100644 --- a/src/components/user_avatar/user_avatar.js +++ b/src/components/user_avatar/user_avatar.js @@ -23,6 +23,11 @@ const UserAvatar = { imageLoadError () { this.showPlaceholder = true } + }, + watch: { + src () { + this.showPlaceholder = false + } } } diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 80d15a27..b07da675 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -1,4 +1,5 @@ import UserAvatar from '../user_avatar/user_avatar.vue' +import RemoteFollow from '../remote_follow/remote_follow.vue' import { hex2rgb } from '../../services/color_convert/color_convert.js' import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -15,6 +16,9 @@ export default { betterShadow: this.$store.state.interface.browserSupport.cssFilter } }, + created () { + this.$store.dispatch('fetchUserRelationship', this.user.id) + }, computed: { classes () { return [{ @@ -96,7 +100,8 @@ export default { } }, components: { - UserAvatar + UserAvatar, + RemoteFollow }, methods: { followUser () { diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index cc2ce6b8..f4114e6e 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -11,7 +11,7 @@ <div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div> <div :title="user.name" class='user-name' v-else>{{user.name}}</div> <router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser"> - <i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i> + <i class="button-icon icon-pencil usersettings" :title="$t('tool_tip.user_settings')"></i> </router-link> <a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local"> <i class="icon-link-ext usersettings"></i> @@ -84,14 +84,8 @@ </button> </span> </div> - <div class="remote-follow" v-if='!loggedIn && user.is_local'> - <form method="POST" :action='subscribeUrl'> - <input type="hidden" name="nickname" :value="user.screen_name"> - <input type="hidden" name="profile" value=""> - <button click="submit" class="remote-button"> - {{ $t('user_card.remote_follow') }} - </button> - </form> + <div v-if='!loggedIn && user.is_local'> + <RemoteFollow :user="user" /> </div> <div class='block' v-if='isOtherUser && loggedIn'> <span v-if='user.statusnet_blocking'> @@ -159,6 +153,18 @@ &-bio { text-align: center; + + img { + object-fit: contain; + vertical-align: middle; + max-width: 100%; + max-height: 400px; + + .emoji { + width: 32px; + height: 32px; + } + } } // Modifiers @@ -363,11 +369,6 @@ min-height: 28px; } - .remote-follow { - max-width: 220px; - min-height: 28px; - } - .follow { max-width: 220px; min-height: 28px; diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 54126514..82df4510 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -9,7 +9,7 @@ import withList from '../../hocs/with_list/with_list' const FollowerList = compose( withLoadMore({ fetch: (props, $store) => $store.dispatch('addFollowers', props.userId), - select: (props, $store) => get($store.getters.userById(props.userId), 'followers', []), + select: (props, $store) => get($store.getters.findUser(props.userId), 'followers', []), destory: (props, $store) => $store.dispatch('clearFollowers', props.userId), childPropName: 'entries', additionalPropNames: ['userId'] @@ -20,7 +20,7 @@ const FollowerList = compose( const FriendList = compose( withLoadMore({ fetch: (props, $store) => $store.dispatch('addFriends', props.userId), - select: (props, $store) => get($store.getters.userById(props.userId), 'friends', []), + select: (props, $store) => get($store.getters.findUser(props.userId), 'friends', []), destory: (props, $store) => $store.dispatch('clearFriends', props.userId), childPropName: 'entries', additionalPropNames: ['userId'] @@ -31,28 +31,16 @@ const FriendList = compose( const UserProfile = { data () { return { - error: false + error: false, + fetchedUserId: null } }, created () { - this.$store.commit('clearTimeline', { timeline: 'user' }) - this.$store.commit('clearTimeline', { timeline: 'favorites' }) - this.$store.commit('clearTimeline', { timeline: 'media' }) - this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy }) - this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy }) - this.startFetchFavorites() if (!this.user.id) { - this.$store.dispatch('fetchUser', this.fetchBy) - .catch((reason) => { - const errorMessage = get(reason, 'error.error') - if (errorMessage === 'No user with such user_id') { // Known error - this.error = this.$t('user_profile.profile_does_not_exist') - } else if (errorMessage) { - this.error = errorMessage - } else { - this.error = this.$t('user_profile.profile_loading_error') - } - }) + this.fetchUserId() + .then(() => this.startUp()) + } else { + this.startUp() } }, destroyed () { @@ -69,7 +57,7 @@ const UserProfile = { return this.$store.state.statuses.timelines.media }, userId () { - return this.$route.params.id || this.user.id + return this.$route.params.id || this.user.id || this.fetchedUserId }, userName () { return this.$route.params.name || this.user.screen_name @@ -79,10 +67,9 @@ const UserProfile = { this.userId === this.$store.state.users.currentUser.id }, userInStore () { - if (this.isExternal) { - return this.$store.getters.userById(this.userId) - } - return this.$store.getters.userByName(this.userName) + const routeParams = this.$route.params + // This needs fetchedUserId so that computed will be refreshed when user is fetched + return this.$store.getters.findUser(this.fetchedUserId || routeParams.name || routeParams.id) }, user () { if (this.timeline.statuses[0]) { @@ -93,9 +80,6 @@ const UserProfile = { } return {} }, - fetchBy () { - return this.isExternal ? this.userId : this.userName - }, isExternal () { return this.$route.name === 'external-user-profile' }, @@ -109,14 +93,38 @@ const UserProfile = { methods: { startFetchFavorites () { if (this.isUs) { - this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.fetchBy }) + this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.userId }) } }, + fetchUserId () { + let fetchPromise + if (this.userId && !this.$route.params.name) { + fetchPromise = this.$store.dispatch('fetchUser', this.userId) + } else { + fetchPromise = this.$store.dispatch('fetchUser', this.userName) + .then(({ id }) => { + this.fetchedUserId = id + }) + } + return fetchPromise + .catch((reason) => { + const errorMessage = get(reason, 'error.error') + if (errorMessage === 'No user with such user_id') { // Known error + this.error = this.$t('user_profile.profile_does_not_exist') + } else if (errorMessage) { + this.error = errorMessage + } else { + this.error = this.$t('user_profile.profile_loading_error') + } + }) + .then(() => this.startUp()) + }, startUp () { - this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy }) - this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy }) - - this.startFetchFavorites() + if (this.userId) { + this.$store.dispatch('startFetching', { timeline: 'user', userId: this.userId }) + this.$store.dispatch('startFetching', { timeline: 'media', userId: this.userId }) + this.startFetchFavorites() + } }, cleanUp () { this.$store.dispatch('stopFetching', 'user') @@ -128,19 +136,19 @@ const UserProfile = { } }, watch: { - userName () { - if (this.isExternal) { - return + // userId can be undefined if we don't know it yet + userId (newVal) { + if (newVal) { + this.cleanUp() + this.startUp() } - this.cleanUp() - this.startUp() }, - userId () { - if (!this.isExternal) { - return + userName () { + if (this.$route.params.name) { + this.fetchUserId() + this.cleanUp() + this.startUp() } - this.cleanUp() - this.startUp() }, $route () { this.$refs.tabSwitcher.activateTab(0)() diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index 7d4a8b1f..d449eb85 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -11,7 +11,7 @@ :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" - :user-id="fetchBy" + :user-id="userId" /> <div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count"> <FriendList :userId="userId" /> @@ -25,7 +25,7 @@ :embedded="true" :title="$t('user_card.media')" timeline-name="media" :timeline="media" - :user-id="fetchBy" + :user-id="userId" /> <Timeline v-if="isUs" diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index c0ab759c..72e7bb53 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -158,7 +158,13 @@ const UserSettings = { reader.readAsDataURL(file) }, submitAvatar (cropper, file) { - const img = cropper.getCroppedCanvas().toDataURL(file.type) + let img + if (cropper) { + img = cropper.getCroppedCanvas().toDataURL(file.type) + } else { + img = file + } + return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => { if (!user.error) { this.$store.commit('addNewUsers', [user]) diff --git a/src/i18n/ar.json b/src/i18n/ar.json index 242dab78..72e3010f 100644 --- a/src/i18n/ar.json +++ b/src/i18n/ar.json @@ -49,7 +49,7 @@ "account_not_locked_warning_link": "مقفل", "attachments_sensitive": "اعتبر المرفقات كلها كمحتوى حساس", "content_type": { - "plain_text": "نص صافٍ" + "text/plain": "نص صافٍ" }, "content_warning": "الموضوع (اختياري)", "default": "وصلت للتوّ إلى لوس أنجلس.", diff --git a/src/i18n/ca.json b/src/i18n/ca.json index d2f285df..8fa3a88b 100644 --- a/src/i18n/ca.json +++ b/src/i18n/ca.json @@ -49,7 +49,7 @@ "account_not_locked_warning_link": "bloquejat", "attachments_sensitive": "Marca l'adjunt com a delicat", "content_type": { - "plain_text": "Text pla" + "text/plain": "Text pla" }, "content_warning": "Assumpte (opcional)", "default": "Em sento…", diff --git a/src/i18n/cs.json b/src/i18n/cs.json index 6326032c..020092a6 100644 --- a/src/i18n/cs.json +++ b/src/i18n/cs.json @@ -71,7 +71,9 @@ "account_not_locked_warning_link": "uzamčen", "attachments_sensitive": "Označovat přílohy jako citlivé", "content_type": { - "plain_text": "Prostý text" + "text/plain": "Prostý text", + "text/html": "HTML", + "text/markdown": "Markdown" }, "content_warning": "Předmět (volitelný)", "default": "Právě jsem přistál v L.A.", @@ -95,7 +97,7 @@ "new_captcha": "Kliknutím na obrázek získáte novou CAPTCHA", "username_placeholder": "např. lain", "fullname_placeholder": "např. Lain Iwakura", - "bio_placeholder": "např.\nNazdar, jsem Lain\nJsem anime dívka a žiji v příměstském Japonsku. Možná mě znáte z Wired.", + "bio_placeholder": "např.\nNazdar, jsem Lain\nJsem anime dívka žijící v příměstském Japonsku. Možná mě znáte z Wired.", "validations": { "username_required": "nemůže být prázdné", "fullname_required": "nemůže být prázdné", @@ -204,7 +206,7 @@ "radii_help": "Nastavit zakulacení rohů rozhraní (v pixelech)", "replies_in_timeline": "Odpovědi v časové ose", "reply_link_preview": "Povolit náhledy odkazu pro odpověď při přejetí myši", - "reply_visibility_all": "Zobrazit všechny odpovědiShow all replies", + "reply_visibility_all": "Zobrazit všechny odpovědi", "reply_visibility_following": "Zobrazit pouze odpovědi směřované na mě nebo uživatele, které sleduji", "reply_visibility_self": "Zobrazit pouze odpovědi směřované na mě", "saving_err": "Chyba při ukládání nastavení", @@ -221,7 +223,6 @@ "subject_line_mastodon": "Jako u Mastodonu: zkopírovat tak, jak je", "subject_line_noop": "Nekopírovat", "post_status_content_type": "Publikovat typ obsahu příspěvku", - "status_content_type_plain": "Prostý text", "stop_gifs": "Přehrávat GIFy při přejetí myši", "streaming": "Povolit automatické streamování nových příspěvků při rolování nahoru", "text": "Text", @@ -339,7 +340,7 @@ "button": "Tlačítko", "text": "Spousta dalšího {0} a {1}", "mono": "obsahu", - "input": "Just landed in L.A.", + "input": "Právě jsem přistál v L.A.", "faint_link": "pomocný manuál", "fine_print": "Přečtěte si náš {0} a nenaučte se nic užitečného!", "header_faint": "Tohle je v pohodě", @@ -361,7 +362,7 @@ "no_statuses": "Žádné příspěvky" }, "status": { - "reply_to": "Odpovědět uživateli", + "reply_to": "Odpověď uživateli", "replies_list": "Odpovědi:" }, @@ -413,7 +414,7 @@ "upload":{ "error": { "base": "Nahrávání selhalo.", - "file_too_big": "Soubor je úříliš velký [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "file_too_big": "Soubor je příliš velký [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", "default": "Zkuste to znovu později" }, "file_size_units": { diff --git a/src/i18n/de.json b/src/i18n/de.json index 07d44348..fa9db16c 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -55,7 +55,7 @@ "account_not_locked_warning_link": "gesperrt", "attachments_sensitive": "Anhänge als heikel markieren", "content_type": { - "plain_text": "Nur Text" + "text/plain": "Nur Text" }, "content_warning": "Betreff (optional)", "default": "Sitze gerade im Hofbräuhaus.", diff --git a/src/i18n/en.json b/src/i18n/en.json index 01fe2fba..68503f99 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -25,6 +25,7 @@ "image_cropper": { "crop_picture": "Crop picture", "save": "Save", + "save_without_cropping": "Save without cropping", "cancel": "Cancel" }, "login": { @@ -347,6 +348,11 @@ "checkbox": "I have skimmed over terms and conditions", "link": "a nice lil' link" } + }, + "version": { + "title": "Version", + "backend_version": "Backend Version", + "frontend_version": "Frontend Version" } }, "timeline": { diff --git a/src/i18n/eo.json b/src/i18n/eo.json index 34851a44..6c5b3a74 100644 --- a/src/i18n/eo.json +++ b/src/i18n/eo.json @@ -71,7 +71,7 @@ "account_not_locked_warning_link": "ŝlosita", "attachments_sensitive": "Marki kunsendaĵojn kiel konsternajn", "content_type": { - "plain_text": "Plata teksto" + "text/plain": "Plata teksto" }, "content_warning": "Temo (malnepra)", "default": "Ĵus alvenis al la Universala Kongreso!", diff --git a/src/i18n/es.json b/src/i18n/es.json index fe96dd08..a692eef9 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -61,7 +61,7 @@ "account_not_locked_warning_link": "bloqueada", "attachments_sensitive": "Contenido sensible", "content_type": { - "plain_text": "Texto Plano" + "text/plain": "Texto Plano" }, "content_warning": "Tema (opcional)", "default": "Acabo de aterrizar en L.A.", diff --git a/src/i18n/fi.json b/src/i18n/fi.json index 4f0ffb4b..fbe676cf 100644 --- a/src/i18n/fi.json +++ b/src/i18n/fi.json @@ -60,7 +60,7 @@ "account_not_locked_warning_link": "lukittu", "attachments_sensitive": "Merkkaa liitteet arkaluonteisiksi", "content_type": { - "plain_text": "Tavallinen teksti" + "text/plain": "Tavallinen teksti" }, "content_warning": "Aihe (valinnainen)", "default": "Tulin juuri saunasta.", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 1209556a..8f9f243e 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -51,7 +51,7 @@ "account_not_locked_warning_link": "verrouillé", "attachments_sensitive": "Marquer le média comme sensible", "content_type": { - "plain_text": "Texte brut" + "text/plain": "Texte brut" }, "content_warning": "Sujet (optionnel)", "default": "Écrivez ici votre prochain statut.", diff --git a/src/i18n/ga.json b/src/i18n/ga.json index 5be9297a..31250876 100644 --- a/src/i18n/ga.json +++ b/src/i18n/ga.json @@ -49,7 +49,7 @@ "account_not_locked_warning_link": "faoi glas", "attachments_sensitive": "Marcáil ceangaltán mar íogair", "content_type": { - "plain_text": "Gnáth-théacs" + "text/plain": "Gnáth-théacs" }, "content_warning": "Teideal (roghnach)", "default": "Lá iontach anseo i nGaillimh", diff --git a/src/i18n/he.json b/src/i18n/he.json index 213e6170..ea581e05 100644 --- a/src/i18n/he.json +++ b/src/i18n/he.json @@ -49,7 +49,7 @@ "account_not_locked_warning_link": "נעול", "attachments_sensitive": "סמן מסמכים מצורפים כלא בטוחים לצפייה", "content_type": { - "plain_text": "טקסט פשוט" + "text/plain": "טקסט פשוט" }, "content_warning": "נושא (נתון לבחירה)", "default": "הרגע נחת ב-ל.א.", diff --git a/src/i18n/it.json b/src/i18n/it.json index 385d21aa..f441292e 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -175,7 +175,7 @@ "account_not_locked_warning_link": "bloccato", "attachments_sensitive": "Segna allegati come sensibili", "content_type": { - "plain_text": "Testo normale" + "text/plain": "Testo normale" }, "content_warning": "Oggetto (facoltativo)", "default": "Appena atterrato in L.A.", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index f39a5a7c..b77f5531 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -61,7 +61,7 @@ "account_not_locked_warning_link": "ロックされたアカウント", "attachments_sensitive": "ファイルをNSFWにする", "content_type": { - "plain_text": "プレーンテキスト" + "text/plain": "プレーンテキスト" }, "content_warning": "せつめい (かかなくてもよい)", "default": "はねだくうこうに、つきました。", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 336e464f..402a354c 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -56,7 +56,7 @@ "account_not_locked_warning_link": "잠김", "attachments_sensitive": "첨부물을 민감함으로 설정", "content_type": { - "plain_text": "평문" + "text/plain": "평문" }, "content_warning": "주제 (필수 아님)", "default": "LA에 도착!", diff --git a/src/i18n/nb.json b/src/i18n/nb.json index 39e054f7..298dc0b9 100644 --- a/src/i18n/nb.json +++ b/src/i18n/nb.json @@ -49,7 +49,7 @@ "account_not_locked_warning_link": "låst", "attachments_sensitive": "Merk vedlegg som sensitive", "content_type": { - "plain_text": "Klar tekst" + "text/plain": "Klar tekst" }, "content_warning": "Tema (valgfritt)", "default": "Landet akkurat i L.A.", diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 799e22b9..7e2f0604 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -57,7 +57,7 @@ "account_not_locked_warning_link": "gesloten", "attachments_sensitive": "Markeer bijlage als gevoelig", "content_type": { - "plain_text": "Gewone tekst" + "text/plain": "Gewone tekst" }, "content_warning": "Onderwerp (optioneel)", "default": "Tijd voor een pauze!", diff --git a/src/i18n/oc.json b/src/i18n/oc.json index fd5ccc97..ecc4df61 100644 --- a/src/i18n/oc.json +++ b/src/i18n/oc.json @@ -59,19 +59,21 @@ "broken_favorite": "Estatut desconegut, sèm a lo cercar...", "favorited_you": "a aimat vòstre estatut", "followed_you": "vos a seguit", - "load_older": "Cargar las notificaciones mai ancianas", + "load_older": "Cargar las notificacions mai ancianas", "notifications": "Notficacions", - "read": "Legit !", + "read": "Legit !", "repeated_you": "a repetit vòstre estatut", "no_more_notifications": "Pas mai de notificacions" }, "post_status": { "new_status": "Publicar d’estatuts novèls", - "account_not_locked_warning": "Vòstre compte es pas {0}. Qual que siá pòt vos seguir per veire vòstras publicacions destinadas pas qu'a vòstres seguidors.", + "account_not_locked_warning": "Vòstre compte es pas {0}. Qual que siá pòt vos seguir per veire vòstras publicacions destinadas pas qu’a vòstres seguidors.", "account_not_locked_warning_link": "clavat", "attachments_sensitive": "Marcar las pèças juntas coma sensiblas", "content_type": { - "plain_text": "Tèxte brut" + "text/plain": "Tèxte brut", + "text/html": "HTML", + "text/markdown": "Markdown" }, "content_warning": "Avís de contengut (opcional)", "default": "Escrivètz aquí vòstre estatut.", @@ -118,12 +120,12 @@ "blocks_tab": "Blocatges", "btnRadius": "Botons", "cBlue": "Blau (Respondre, seguir)", - "cGreen": "Verd (Repartajar)", + "cGreen": "Verd (Repertir)", "cOrange": "Irange (Aimar)", "cRed": "Roge (Anullar)", "change_password": "Cambiar lo senhal", "change_password_error": "Una error s’es producha en cambiant lo senhal.", - "changed_password": "Senhal corrèctament cambiat !", + "changed_password": "Senhal corrèctament cambiat !", "collapse_subject": "Replegar las publicacions amb de subjèctes", "composing": "Escritura", "confirm_new_password": "Confirmatz lo nòu senhal", @@ -134,7 +136,7 @@ "default_vis": "Nivèl de visibilitat per defaut", "delete_account": "Suprimir lo compte", "delete_account_description": "Suprimir vòstre compte e los messatges per sempre.", - "delete_account_error": "Una error s’es producha en suprimir lo compte. S’aquò ten d’arribar mercés de contactar vòstre administrador d’instància.", + "delete_account_error": "Una error s’es producha en suprimir lo compte. S’aquò ten d’arribar mercés de contactar vòstre administrator d’instància.", "delete_account_instructions": "Picatz vòstre senhal dins lo camp tèxte çai-jos per confirmar la supression del compte.", "avatar_size_instruction": "La talha minimum recomandada pels imatges d’avatar es 150x150 pixèls.", "export_theme": "Enregistrar la preconfiguracion", @@ -154,14 +156,14 @@ "hide_isp": "Amagar lo panèl especial instància", "preload_images": "Precargar los imatges", "use_one_click_nsfw": "Dobrir las pèças juntas NSFW amb un clic", - "hide_post_stats": "Amagar los estatistics de publicacion (ex. lo ombre de favorits)", + "hide_post_stats": "Amagar las estatisticas de publicacion (ex. lo nombre de favorits)", "hide_user_stats": "Amagar las estatisticas de l’utilizaire (ex. lo nombre de seguidors)", "hide_filtered_statuses": "Amagar los estatuts filtrats", "import_followers_from_a_csv_file": "Importar los seguidors d’un fichièr csv", "import_theme": "Cargar un tèma", "inputRadius": "Camps tèxte", "checkboxRadius": "Casas de marcar", - "instance_default": "(defaut : {value})", + "instance_default": "(defaut : {value})", "instance_default_simple": "(defaut)", "interface": "Interfàcia", "interfaceLanguage": "Lenga de l’interfàcia", @@ -172,7 +174,7 @@ "loop_video": "Bocla vidèo", "loop_video_silent_only": "Legir en bocla solament las vidèos sens son (coma los « Gifs » de Mastodon)", "mutes_tab": "Agamats", - "play_videos_in_modal": "Legir las vidèoas dirèctament dins la visualizaira mèdia", + "play_videos_in_modal": "Legir las vidèos dirèctament dins la visualizaira mèdia", "use_contain_fit": "Talhar pas las pèças juntas per las vinhetas", "name": "Nom", "name_bio": "Nom & Bio", @@ -223,7 +225,7 @@ "post_status_content_type": "Publicar lo tipe de contengut dels estatuts", "stop_gifs": "Lançar los GIFs al subrevòl", "streaming": "Activar lo cargament automatic dels novèls estatus en anar amont", - "text": "Tèxt", + "text": "Tèxte", "theme": "Tèma", "theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.", "theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.", @@ -234,6 +236,117 @@ "values": { "false": "non", "true": "òc" + }, + "notifications": "Notificacions", + "enable_web_push_notifications": "Activar las notificacions web push", + "style": { + "switcher": { + "keep_color": "Gardar las colors", + "keep_shadows": "Gardar las ombras", + "keep_opacity": "Gardar l’opacitat", + "keep_roundness": "Gardar la redondetat", + "keep_fonts": "Gardar las polissas", + "save_load_hint": "Las opcions « Gardar » permeton de servar las opcions configuradas actualament quand seleccionatz o cargatz un tèma, permeton tanben d’enregistrar aquelas opcions quand exportatz un tèma. Quand totas las casas son pas marcadas, l’exportacion de tèma o enregistrarà tot.", + "reset": "Restablir", + "clear_all": "O escafar tot", + "clear_opacity": "Escafar l’opacitat" + }, + "common": { + "color": "Color", + "opacity": "Opacitat", + "contrast": { + "hint": "Lo coeficient de contraste es de {ratio}. Dòna {level} {context}", + "level": { + "aa": "un nivèl AA minimum recomandat", + "aaa": "un nivèl AAA recomandat", + "bad": "pas un nivèl d’accessibilitat recomandat" + }, + "context": { + "18pt": "pel tèxte grand (18pt+)", + "text": "pel tèxte" + } + } + }, + "common_colors": { + "_tab_label": "Comun", + "main": "Colors comunas", + "foreground_hint": "Vejatz « Avançat » per mai de paramètres detalhats", + "rgbo": "Icònas, accents, badges" + }, + "advanced_colors": { + "_tab_label": "Avançat", + "alert": "Rèire plan d’alèrtas", + "alert_error": "Error", + "badge": "Rèire plan dels badges", + "badge_notification": "Notificacion", + "panel_header": "Bandièra del tablèu de bòrd", + "top_bar": "Barra amont", + "borders": "Caires", + "buttons": "Botons", + "inputs": "Camps tèxte", + "faint_text": "Tèxte descolorit" + }, + "radii": { + "_tab_label": "Redondetat" + }, + "shadows": { + "_tab_label": "Ombra e luminositat", + "component": "Compausant", + "override": "Subrecargar", + "shadow_id": "Ombra #{value}", + "blur": "Fosc", + "spread": "Espandiment", + "inset": "Incrustacion", + "hint": "Per las ombras podètz tanben utilizar --variable coma valor de color per emplegar una variable CSS3. Notatz que lo paramètre d’opacitat foncionarà pas dins aquel cas.", + "filter_hint": { + "always_drop_shadow": "Avertiment, aquel ombra utiliza totjorn {0} quand lo navigator es compatible.", + "drop_shadow_syntax": "{0} es pas compatible amb lo paramètre {1} e lo mot clau {2}.", + "avatar_inset": "Notatz que combinar d’ombras incrustadas e pas incrustadas pòt donar de resultats inesperats amb los avatars transparents.", + "spread_zero": "L’ombra amb un espandiment de > 0 apareisserà coma reglat a zèro", + "inset_classic": "L’ombra d’incrustacion utilizarà {0}" + }, + "components": { + "panel": "Tablèu", + "panelHeader": "Bandièra del tablèu", + "topBar": "Barra amont", + "avatar": "Utilizar l’avatar (vista perfil)", + "avatarStatus": "Avatar de l’utilizaire (afichatge publicacion)", + "popup": "Fenèstras sorgissentas e astúcias", + "button": "Boton", + "buttonHover": "Boton (en passar la mirga)", + "buttonPressed": "Boton (en quichar)", + "buttonPressedHover": "Boton (en quichar e passar)", + "input": "Camp tèxte" + } + }, + "fonts": { + "_tab_label": "Polissas", + "help": "Selecionatz la polissa d’utilizar pels elements de l’UI. Per « Personalizada » vos cal picar lo nom exacte tal coma apareis sul sistèma.", + "components": { + "interface": "Interfàcia", + "input": "Camps tèxte", + "post": "Tèxte de publicacion", + "postCode": "Tèxte Monospaced dins las publicacion (tèxte formatat)" + }, + "family": "Nom de la polissa", + "size": "Talha (en px)", + "weight": "Largor (gras)", + "custom": "Personalizada" + }, + "preview": { + "header": "Apercebut", + "content": "Contengut", + "error": "Error d’exemple", + "button": "Boton", + "text": "A tròç de mai de {0} e {1}", + "mono": "contengut", + "input": "arribada al país.", + "faint_link": "manual d’ajuda", + "fine_print": "Legissètz nòstre {0} per legir pas res d’util !", + "header_faint": "Va plan", + "checkbox": "Ai legit los tèrmes e condicions d’utilizacion", + "link": "un pichon ligam simpatic" + } } }, "timeline": { @@ -241,19 +354,21 @@ "conversation": "Conversacion", "error_fetching": "Error en cercant de mesas a jorn", "load_older": "Ne veire mai", + "no_retweet_hint": "Las publicacions marcadas pels seguidors solament o dirèctas se pòdon pas repetir", "repeated": "repetit", "show_new": "Ne veire mai", "up_to_date": "A jorn", - "no_retweet_hint": "La publicacion marcada coma pels seguidors solament o dirècte pòt pas èsser repetida" + "no_more_statuses": "Pas mai d’estatuts", + "no_statuses": "Cap d’estatuts" }, "status": { - "reply_to": "Respondre à", + "reply_to": "Respond a", "replies_list": "Responsas :" }, "user_card": { "approve": "Validar", "block": "Blocar", - "blocked": "Blocat !", + "blocked": "Blocat !", "deny": "Refusar", "favorites": "Favorits", "follow": "Seguir", @@ -263,8 +378,8 @@ "follow_unfollow": "Quitar de seguir", "followees": "Abonaments", "followers": "Seguidors", - "following": "Seguit !", - "follows_you": "Vos sèc !", + "following": "Seguit !", + "follows_you": "Vos sèc !", "its_you": "Sètz vos !", "media": "Mèdia", "mute": "Amagar", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index cbc2c9a3..41a34483 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -51,7 +51,7 @@ "public_tl": "Linha do tempo pública", "timeline": "Linha do tempo", "twkn": "Toda a rede conhecida", - "user_search": "Busca de usuário", + "user_search": "Buscar usuários", "who_to_follow": "Quem seguir", "preferences": "Preferências" }, @@ -67,11 +67,11 @@ }, "post_status": { "new_status": "Postar novo status", - "account_not_locked_warning": "Sua conta não está {0}. Qualquer pessoa pode te seguir para ver seus posts restritos.", - "account_not_locked_warning_link": "fechada", + "account_not_locked_warning": "Sua conta não é {0}. Qualquer pessoa pode te seguir e ver seus posts privados (só para seguidores).", + "account_not_locked_warning_link": "restrita", "attachments_sensitive": "Marcar anexos como sensíveis", "content_type": { - "plain_text": "Texto puro" + "text/plain": "Texto puro" }, "content_warning": "Assunto (opcional)", "default": "Acabei de chegar no Rio!", @@ -115,7 +115,7 @@ "avatarRadius": "Avatares", "background": "Pano de Fundo", "bio": "Biografia", - "blocks_tab": "Blocos", + "blocks_tab": "Bloqueios", "btnRadius": "Botões", "cBlue": "Azul (Responder, seguir)", "cGreen": "Verde (Repetir)", @@ -125,7 +125,7 @@ "change_password_error": "Houve um erro ao modificar sua senha.", "changed_password": "Senha modificada com sucesso!", "collapse_subject": "Esconder posts com assunto", - "composing": "Escrevendo", + "composing": "Escrita", "confirm_new_password": "Confirmar nova senha", "current_avatar": "Seu avatar atual", "current_password": "Sua senha atual", @@ -139,7 +139,7 @@ "avatar_size_instruction": "O tamanho mínimo recomendado para imagens de avatar é 150x150 pixels.", "export_theme": "Salvar predefinições", "filtering": "Filtragem", - "filtering_explanation": "Todas as postagens contendo estas palavras serão silenciadas, uma por linha.", + "filtering_explanation": "Todas as postagens contendo estas palavras serão silenciadas; uma palavra por linha.", "follow_export": "Exportar quem você segue", "follow_export_button": "Exportar quem você segue para um arquivo CSV", "follow_export_processing": "Processando. Em breve você receberá a solicitação de download do arquivo", @@ -178,7 +178,7 @@ "name_bio": "Nome & Biografia", "new_password": "Nova senha", "notification_visibility": "Tipos de notificação para mostrar", - "notification_visibility_follows": "Seguidos", + "notification_visibility_follows": "Seguidas", "notification_visibility_likes": "Favoritos", "notification_visibility_mentions": "Menções", "notification_visibility_repeats": "Repetições", @@ -187,7 +187,7 @@ "no_mutes": "Sem silenciados", "hide_follows_description": "Não mostrar quem estou seguindo", "hide_followers_description": "Não mostrar quem me segue", - "show_admin_badge": "Mostrar distintivo de Administrador em meu perfil", + "show_admin_badge": "Mostrar título de Administrador em meu perfil", "show_moderator_badge": "Mostrar título de Moderador em meu perfil", "nsfw_clickthrough": "Habilitar clique para ocultar anexos sensíveis", "oauth_tokens": "Token OAuth", @@ -201,9 +201,9 @@ "profile_background": "Pano de fundo de perfil", "profile_banner": "Capa de perfil", "profile_tab": "Perfil", - "radii_help": "Arredondar arestas da interface (em píxeis)", + "radii_help": "Arredondar arestas da interface (em pixel)", "replies_in_timeline": "Respostas na linha do tempo", - "reply_link_preview": "Habilitar a pré-visualização de link de respostas ao passar o mouse.", + "reply_link_preview": "Habilitar a pré-visualização de de respostas ao passar o mouse.", "reply_visibility_all": "Mostrar todas as respostas", "reply_visibility_following": "Só mostrar respostas direcionadas a mim ou a usuários que sigo", "reply_visibility_self": "Só mostrar respostas direcionadas a mim", @@ -212,7 +212,7 @@ "security_tab": "Segurança", "scope_copy": "Copiar opções de privacidade ao responder (Mensagens diretas sempre copiam)", "set_new_avatar": "Alterar avatar", - "set_new_profile_background": "Alterar o plano de fundo de perfil", + "set_new_profile_background": "Alterar o pano de fundo de perfil", "set_new_profile_banner": "Alterar capa de perfil", "settings": "Configurações", "subject_input_always_show": "Sempre mostrar campo de assunto", @@ -220,9 +220,9 @@ "subject_line_email": "Como em email: \"re: assunto\"", "subject_line_mastodon": "Como o Mastodon: copiar como está", "subject_line_noop": "Não copiar", - "post_status_content_type": "Postar tipo de conteúdo do status", - "stop_gifs": "Reproduzir GIFs ao passar o cursor em cima", - "streaming": "Habilitar o fluxo automático de postagens quando ao topo da página", + "post_status_content_type": "Tipo de conteúdo do status", + "stop_gifs": "Reproduzir GIFs ao passar o cursor", + "streaming": "Habilitar o fluxo automático de postagens no topo da página", "text": "Texto", "theme": "Tema", "theme_help": "Use cores em código hexadecimal (#rrggbb) para personalizar seu esquema de cores.", @@ -235,7 +235,7 @@ "false": "não", "true": "sim" }, - "notifications": "Notifications", + "notifications": "Notificações", "enable_web_push_notifications": "Habilitar notificações web push", "style": { "switcher": { @@ -245,7 +245,7 @@ "keep_roundness": "Manter arredondado", "keep_fonts": "Manter fontes", "save_load_hint": "Manter as opções preserva as opções atuais ao selecionar ou carregar temas; também salva as opções ao exportar um tempo. Quanto todos os campos estiverem desmarcados, tudo será salvo ao exportar o tema.", - "reset": "Voltar ao padrão", + "reset": "Restaurar o padrão", "clear_all": "Limpar tudo", "clear_opacity": "Limpar opacidade" }, @@ -319,7 +319,7 @@ }, "fonts": { "_tab_label": "Fontes", - "help": "Selecionar fonte dos elementos da interface. Para fonte \"personalizada\" você deve entrar exatamente o nome da fonte no sistema.", + "help": "Selecione as fontes dos elementos da interface. Para fonte \"personalizada\" você deve inserir o mesmo nome da fonte no sistema.", "components": { "interface": "Interface", "input": "Campo de entrada", @@ -383,7 +383,7 @@ "mute": "Silenciar", "muted": "Silenciado", "per_day": "por dia", - "remote_follow": "Seguidor Remoto", + "remote_follow": "Seguir remotamente", "statuses": "Postagens", "unblock": "Desbloquear", "unblock_progress": "Desbloqueando...", diff --git a/src/i18n/zh.json b/src/i18n/zh.json index 089a98e2..da6dae5f 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -49,7 +49,7 @@ "account_not_locked_warning_link": "上锁", "attachments_sensitive": "标记附件为敏感内容", "content_type": { - "plain_text": "纯文本" + "text/plain": "纯文本" }, "content_warning": "主题(可选)", "default": "刚刚抵达上海", diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index e828a74b..7ab89c12 100644 --- a/src/lib/persisted_state.js +++ b/src/lib/persisted_state.js @@ -60,18 +60,6 @@ export default function createPersistedState ({ merge({}, store.state, savedState) ) } - if (store.state.config.customTheme) { - // This is a hack to deal with async loading of config.json and themes - // See: style_setter.js, setPreset() - window.themeLoaded = true - store.dispatch('setOption', { - name: 'customTheme', - value: store.state.config.customTheme - }) - } - if (store.state.oauth.token) { - store.dispatch('loginUser', store.state.oauth.token) - } loaded = true } catch (e) { console.log("Couldn't load state") diff --git a/src/main.js b/src/main.js index a3265e3a..9ffc3727 100644 --- a/src/main.js +++ b/src/main.js @@ -53,9 +53,10 @@ const persistedStateOptions = { 'users.lastLoginName', 'oauth' ] -} +}; -createPersistedState(persistedStateOptions).then((persistedState) => { +(async () => { + const persistedState = await createPersistedState(persistedStateOptions) const store = new Vuex.Store({ modules: { interface: interfaceModule, @@ -75,7 +76,7 @@ createPersistedState(persistedStateOptions).then((persistedState) => { }) afterStoreSetup({ store, i18n }) -}) +})() // These are inlined by webpack's DefinePlugin /* eslint-disable */ diff --git a/src/modules/chat.js b/src/modules/chat.js index 383ac75c..2804e577 100644 --- a/src/modules/chat.js +++ b/src/modules/chat.js @@ -1,12 +1,16 @@ const chat = { state: { messages: [], - channel: {state: ''} + channel: {state: ''}, + socket: null }, mutations: { setChannel (state, channel) { state.channel = channel }, + setSocket (state, socket) { + state.socket = socket + }, addMessage (state, message) { state.messages.push(message) state.messages = state.messages.slice(-19, 20) @@ -16,8 +20,12 @@ const chat = { } }, actions: { + disconnectFromChat (store) { + store.state.socket.disconnect() + }, initializeChat (store, socket) { const channel = socket.channel('chat:public') + store.commit('setSocket', socket) channel.on('new_msg', (msg) => { store.commit('addMessage', msg) }) diff --git a/src/modules/instance.js b/src/modules/instance.js index 24c52f9c..155aa2eb 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -48,7 +48,11 @@ const defaultState = { // Html stuff instanceSpecificPanelContent: '', - tos: '' + tos: '', + + // Version Information + backendVersion: '', + frontendVersion: '' } const instance = { diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 7571b62a..f14b8703 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -1,4 +1,4 @@ -import { remove, slice, each, find, maxBy, minBy, merge, first, last, isArray } from 'lodash' +import { remove, slice, each, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash' import apiService from '../services/api/api.service.js' // import parse from '../services/status_parser/status_parser.js' @@ -72,7 +72,9 @@ const mergeOrAdd = (arr, obj, item) => { if (oldItem) { // We already have this, so only merge the new info. - merge(oldItem, item) + // We ignore null values to avoid overwriting existing properties with missing data + // we also skip 'user' because that is handled by users module + merge(oldItem, omitBy(item, (v, k) => v === null || k === 'user')) // Reactivity fix. oldItem.attachments.splice(oldItem.attachments.length) return {item: oldItem, new: false} @@ -333,6 +335,7 @@ export const mutations = { oldTimeline.newStatusCount = 0 oldTimeline.visibleStatuses = slice(oldTimeline.statuses, 0, 50) oldTimeline.minVisibleId = last(oldTimeline.visibleStatuses).id + oldTimeline.minId = oldTimeline.minVisibleId oldTimeline.visibleStatusesObject = {} each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status }) }, diff --git a/src/modules/users.js b/src/modules/users.js index 4159964c..1fe12fc8 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -18,7 +18,7 @@ export const mergeOrAdd = (arr, obj, item) => { arr.push(item) obj[item.id] = item if (item.screen_name && !item.screen_name.includes('@')) { - obj[item.screen_name] = item + obj[item.screen_name.toLowerCase()] = item } return { item, new: true } } @@ -91,6 +91,17 @@ export const mutations = { addNewUsers (state, users) { each(users, (user) => mergeOrAdd(state.users, state.usersObject, user)) }, + updateUserRelationship (state, relationships) { + relationships.forEach((relationship) => { + const user = state.usersObject[relationship.id] + if (user) { + user.follows_you = relationship.followed_by + user.following = relationship.following + user.muted = relationship.muting + user.statusnet_blocking = relationship.blocking + } + }) + }, saveBlocks (state, blockIds) { state.currentUser.blockIds = blockIds }, @@ -122,12 +133,14 @@ export const mutations = { } export const getters = { - userById: state => id => - state.users.find(user => user.id === id), - userByName: state => name => - state.users.find(user => user.screen_name && - (user.screen_name.toLowerCase() === name.toLowerCase()) - ) + findUser: state => query => { + const result = state.usersObject[query] + // In case it's a screen_name, we can try searching case-insensitive + if (!result && typeof query === 'string') { + return state.usersObject[query.toLowerCase()] + } + return result + } } export const defaultState = { @@ -147,7 +160,14 @@ const users = { actions: { fetchUser (store, id) { return store.rootState.api.backendInteractor.fetchUser({ id }) - .then((user) => store.commit('addNewUsers', [user])) + .then((user) => { + store.commit('addNewUsers', [user]) + return user + }) + }, + fetchUserRelationship (store, id) { + return store.rootState.api.backendInteractor.fetchUserRelationship({ id }) + .then((relationships) => store.commit('updateUserRelationship', relationships)) }, fetchBlocks (store) { return store.rootState.api.backendInteractor.fetchBlocks() @@ -292,6 +312,7 @@ const users = { logout (store) { store.commit('clearCurrentUser') + store.dispatch('disconnectFromChat') store.commit('setToken', false) store.dispatch('stopFetching', 'friends') store.commit('setBackendInteractor', backendInteractorService()) @@ -321,6 +342,9 @@ const users = { if (user.token) { store.dispatch('setWsToken', user.token) + + // Initialize the chat socket. + store.dispatch('initializeSocket') } // Start getting fresh posts. diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index fe2cb1c8..9393e6f1 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -27,12 +27,10 @@ const BG_UPDATE_URL = '/api/qvitter/update_background_image.json' const BANNER_UPDATE_URL = '/api/account/update_profile_banner.json' const PROFILE_UPDATE_URL = '/api/account/update_profile.json' const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json' -const QVITTER_USER_TIMELINE_URL = '/api/qvitter/statuses/user_timeline.json' const QVITTER_USER_NOTIFICATIONS_URL = '/api/qvitter/statuses/notifications.json' const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json' const BLOCKING_URL = '/api/blocks/create.json' const UNBLOCKING_URL = '/api/blocks/destroy.json' -const USER_URL = '/api/users/show.json' const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import' const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account' const CHANGE_PASSWORD_URL = '/api/pleroma/change_password' @@ -43,6 +41,9 @@ const SUGGESTIONS_URL = '/api/v1/suggestions' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home' +const MASTODON_USER_URL = '/api/v1/accounts' +const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships' +const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses` import { each, map } from 'lodash' import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js' @@ -243,7 +244,7 @@ const denyUser = ({id, credentials}) => { } const fetchUser = ({id, credentials}) => { - let url = `${USER_URL}?user_id=${id}` + let url = `${MASTODON_USER_URL}/${id}` return fetch(url, { headers: authHeaders(credentials) }) .then((response) => { return new Promise((resolve, reject) => response.json() @@ -257,6 +258,20 @@ const fetchUser = ({id, credentials}) => { .then((data) => parseUser(data)) } +const fetchUserRelationship = ({id, credentials}) => { + let url = `${MASTODON_USER_RELATIONSHIPS_URL}/?id=${id}` + return fetch(url, { headers: authHeaders(credentials) }) + .then((response) => { + return new Promise((resolve, reject) => response.json() + .then((json) => { + if (!response.ok) { + return reject(new StatusCodeError(response.status, json, { url }, response)) + } + return resolve(json) + })) + }) +} + const fetchFriends = ({id, page, credentials}) => { let url = `${FRIENDS_URL}?user_id=${id}` if (page) { @@ -347,8 +362,8 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use dms: DM_TIMELINE_URL, notifications: QVITTER_USER_NOTIFICATIONS_URL, 'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL, - user: QVITTER_USER_TIMELINE_URL, - media: QVITTER_USER_TIMELINE_URL, + user: MASTODON_USER_TIMELINE_URL, + media: MASTODON_USER_TIMELINE_URL, favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, tag: TAG_TIMELINE_URL } @@ -357,15 +372,16 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use let url = timelineUrls[timeline] + if (timeline === 'user' || timeline === 'media') { + url = url(userId) + } + if (since) { params.push(['since_id', since]) } if (until) { params.push(['max_id', until]) } - if (userId) { - params.push(['user_id', userId]) - } if (tag) { url += `/${tag}.json` } @@ -545,7 +561,12 @@ const fetchOAuthTokens = ({credentials}) => { return fetch(url, { headers: authHeaders(credentials) - }).then((data) => data.json()) + }).then((data) => { + if (data.ok) { + return data.json() + } + throw new Error('Error fetching auth tokens', data) + }) } const revokeOAuthToken = ({id, credentials}) => { @@ -588,6 +609,7 @@ const apiService = { blockUser, unblockUser, fetchUser, + fetchUserRelationship, favorite, unfavorite, retweet, diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 7e972d7b..cbd0b733 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -30,6 +30,10 @@ const backendInteractorService = (credentials) => { return apiService.fetchUser({id, credentials}) } + const fetchUserRelationship = ({id}) => { + return apiService.fetchUserRelationship({id, credentials}) + } + const followUser = (id) => { return apiService.followUser({credentials, id}) } @@ -92,6 +96,7 @@ const backendInteractorService = (credentials) => { blockUser, unblockUser, fetchUser, + fetchUserRelationship, fetchAllFollowing, verifyCredentials: apiService.verifyCredentials, startFetching, diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index d20ce77f..e831963a 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -39,11 +39,11 @@ export const parseUser = (data) => { return output } - output.name = null // missing - output.name_html = data.display_name + // output.name = ??? missing + output.name_html = addEmojis(data.display_name, data.emojis) - output.description = null // missing - output.description_html = data.note + // output.description = ??? missing + output.description_html = addEmojis(data.note, data.emojis) // Utilize avatar_static for gif avatars? output.profile_image_url = data.avatar @@ -59,10 +59,14 @@ export const parseUser = (data) => { output.statusnet_profile_url = data.url if (data.pleroma) { - const pleroma = data.pleroma - output.follows_you = pleroma.follows_you - output.statusnet_blocking = pleroma.statusnet_blocking - output.muted = pleroma.muted + const relationship = data.pleroma.relationship + + if (relationship) { + output.follows_you = relationship.followed_by + output.following = relationship.following + output.statusnet_blocking = relationship.blocking + output.muted = relationship.muting + } } // Missing, trying to recover @@ -83,7 +87,7 @@ export const parseUser = (data) => { output.friends_count = data.friends_count - output.bot = null // missing + // output.bot = ??? missing output.statusnet_profile_url = data.statusnet_profile_url @@ -134,7 +138,7 @@ const parseAttachment = (data) => { output.meta = data.meta // not present in BE yet } else { output.mimetype = data.mimetype - output.meta = null // missing + // output.meta = ??? missing } output.url = data.url @@ -142,6 +146,14 @@ const parseAttachment = (data) => { return output } +export const addEmojis = (string, emojis) => { + return emojis.reduce((acc, emoji) => { + return acc.replace( + new RegExp(`:${emoji.shortcode}:`, 'g'), + `<img src='${emoji.url}' alt='${emoji.shortcode}' class='emoji' />` + ) + }, string) +} export const parseStatus = (data) => { const output = {} @@ -157,7 +169,7 @@ export const parseStatus = (data) => { output.type = data.reblog ? 'retweet' : 'status' output.nsfw = data.sensitive - output.statusnet_html = data.content + output.statusnet_html = addEmojis(data.content, data.emojis) // Not exactly the same but works? output.text = data.content @@ -166,7 +178,7 @@ export const parseStatus = (data) => { output.in_reply_to_user_id = data.in_reply_to_account_id // Missing!! fix in UI? - output.in_reply_to_screen_name = null + // output.in_reply_to_screen_name = ??? // Not exactly the same but works output.statusnet_conversation_id = data.id @@ -176,11 +188,10 @@ export const parseStatus = (data) => { } output.summary = data.spoiler_text - output.summary_html = data.spoiler_text + output.summary_html = addEmojis(data.spoiler_text, data.emojis) output.external_url = data.url - // FIXME missing!! - output.is_local = false + // output.is_local = ??? missing } else { output.favorited = data.favorited output.fave_num = data.fave_num @@ -259,7 +270,7 @@ export const parseNotification = (data) => { if (masto) { output.type = mastoDict[data.type] || data.type - output.seen = null // missing + // output.seen = ??? missing output.status = parseStatus(data.status) output.action = output.status // not sure output.from_profile = parseUser(data.account) diff --git a/src/services/version/version.service.js b/src/services/version/version.service.js new file mode 100644 index 00000000..a750b0dd --- /dev/null +++ b/src/services/version/version.service.js @@ -0,0 +1,6 @@ + +export const extractCommit = versionString => { + const regex = /-g(\w+)$/i + const matches = versionString.match(regex) + return matches ? matches[1] : '' +} diff --git a/static/font/LICENSE.txt b/static/font/LICENSE.txt old mode 100644 new mode 100755 diff --git a/static/font/README.txt b/static/font/README.txt old mode 100644 new mode 100755 diff --git a/static/font/config.json b/static/font/config.json old mode 100644 new mode 100755 index f16b8029..d72b622c --- a/static/font/config.json +++ b/static/font/config.json @@ -233,6 +233,12 @@ "css": "play-circled", "code": 61764, "src": "fontawesome" + }, + { + "uid": "d35a1d35efeb784d1dc9ac18b9b6c2b6", + "css": "pencil", + "code": 59416, + "src": "fontawesome" } ] } \ No newline at end of file diff --git a/static/font/css/animation.css b/static/font/css/animation.css old mode 100644 new mode 100755 diff --git a/static/font/css/fontello-codes.css b/static/font/css/fontello-codes.css old mode 100644 new mode 100755 index cdc21ef3..49175c8f --- a/static/font/css/fontello-codes.css +++ b/static/font/css/fontello-codes.css @@ -23,6 +23,7 @@ .icon-plus:before { content: '\e815'; } /* '' */ .icon-adjust:before { content: '\e816'; } /* '' */ .icon-edit:before { content: '\e817'; } /* '' */ +.icon-pencil:before { content: '\e818'; } /* '' */ .icon-spin3:before { content: '\e832'; } /* '' */ .icon-spin4:before { content: '\e834'; } /* '' */ .icon-link-ext:before { content: '\f08e'; } /* '' */ diff --git a/static/font/css/fontello-embedded.css b/static/font/css/fontello-embedded.css old mode 100644 new mode 100755 index b24597b2..c43ad321 --- a/static/font/css/fontello-embedded.css +++ b/static/font/css/fontello-embedded.css @@ -1,15 +1,15 @@ @font-face { font-family: 'fontello'; - src: url('../font/fontello.eot?50735214'); - src: url('../font/fontello.eot?50735214#iefix') format('embedded-opentype'), - url('../font/fontello.svg?50735214#fontello') format('svg'); + src: url('../font/fontello.eot?21048049'); + src: url('../font/fontello.eot?21048049#iefix') format('embedded-opentype'), + url('../font/fontello.svg?21048049#fontello') format('svg'); font-weight: normal; font-style: normal; } @font-face { font-family: 'fontello'; - src: url('data:application/octet-stream;base64,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') format('woff'), - url('data:application/octet-stream;base64,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') format('truetype'); + src: url('data:application/octet-stream;base64,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') format('woff'), + url('data:application/octet-stream;base64,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') format('truetype'); } /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ @@ -17,7 +17,7 @@ @media screen and (-webkit-min-device-pixel-ratio:0) { @font-face { font-family: 'fontello'; - src: url('../font/fontello.svg?50735214#fontello') format('svg'); + src: url('../font/fontello.svg?21048049#fontello') format('svg'); } } */ @@ -76,6 +76,7 @@ .icon-plus:before { content: '\e815'; } /* '' */ .icon-adjust:before { content: '\e816'; } /* '' */ .icon-edit:before { content: '\e817'; } /* '' */ +.icon-pencil:before { content: '\e818'; } /* '' */ .icon-spin3:before { content: '\e832'; } /* '' */ .icon-spin4:before { content: '\e834'; } /* '' */ .icon-link-ext:before { content: '\f08e'; } /* '' */ diff --git a/static/font/css/fontello-ie7-codes.css b/static/font/css/fontello-ie7-codes.css old mode 100644 new mode 100755 index 638813cd..56e11447 --- a/static/font/css/fontello-ie7-codes.css +++ b/static/font/css/fontello-ie7-codes.css @@ -23,6 +23,7 @@ .icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } diff --git a/static/font/css/fontello-ie7.css b/static/font/css/fontello-ie7.css old mode 100644 new mode 100755 index decbcc43..edced9cb --- a/static/font/css/fontello-ie7.css +++ b/static/font/css/fontello-ie7.css @@ -34,6 +34,7 @@ .icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } diff --git a/static/font/css/fontello.css b/static/font/css/fontello.css old mode 100644 new mode 100755 index 63630d13..64a7a938 --- a/static/font/css/fontello.css +++ b/static/font/css/fontello.css @@ -1,11 +1,11 @@ @font-face { font-family: 'fontello'; - src: url('../font/fontello.eot?94672585'); - src: url('../font/fontello.eot?94672585#iefix') format('embedded-opentype'), - url('../font/fontello.woff2?94672585') format('woff2'), - url('../font/fontello.woff?94672585') format('woff'), - url('../font/fontello.ttf?94672585') format('truetype'), - url('../font/fontello.svg?94672585#fontello') format('svg'); + src: url('../font/fontello.eot?40679575'); + src: url('../font/fontello.eot?40679575#iefix') format('embedded-opentype'), + url('../font/fontello.woff2?40679575') format('woff2'), + url('../font/fontello.woff?40679575') format('woff'), + url('../font/fontello.ttf?40679575') format('truetype'), + url('../font/fontello.svg?40679575#fontello') format('svg'); font-weight: normal; font-style: normal; } @@ -15,7 +15,7 @@ @media screen and (-webkit-min-device-pixel-ratio:0) { @font-face { font-family: 'fontello'; - src: url('../font/fontello.svg?94672585#fontello') format('svg'); + src: url('../font/fontello.svg?40679575#fontello') format('svg'); } } */ @@ -79,6 +79,7 @@ .icon-plus:before { content: '\e815'; } /* '' */ .icon-adjust:before { content: '\e816'; } /* '' */ .icon-edit:before { content: '\e817'; } /* '' */ +.icon-pencil:before { content: '\e818'; } /* '' */ .icon-spin3:before { content: '\e832'; } /* '' */ .icon-spin4:before { content: '\e834'; } /* '' */ .icon-link-ext:before { content: '\f08e'; } /* '' */ diff --git a/static/font/demo.html b/static/font/demo.html old mode 100644 new mode 100755 index 9015b359..2c89a505 --- a/static/font/demo.html +++ b/static/font/demo.html @@ -229,11 +229,11 @@ body { } @font-face { font-family: 'fontello'; - src: url('./font/fontello.eot?28736547'); - src: url('./font/fontello.eot?28736547#iefix') format('embedded-opentype'), - url('./font/fontello.woff?28736547') format('woff'), - url('./font/fontello.ttf?28736547') format('truetype'), - url('./font/fontello.svg?28736547#fontello') format('svg'); + src: url('./font/fontello.eot?50378338'); + src: url('./font/fontello.eot?50378338#iefix') format('embedded-opentype'), + url('./font/fontello.woff?50378338') format('woff'), + url('./font/fontello.ttf?50378338') format('truetype'), + url('./font/fontello.svg?50378338#fontello') format('svg'); font-weight: normal; font-style: normal; } @@ -334,24 +334,25 @@ body { <div class="the-icons span3" title="Code: 0xe817"><i class="demo-icon icon-edit"></i> <span class="i-name">icon-edit</span><span class="i-code">0xe817</span></div> </div> <div class="row"> + <div class="the-icons span3" title="Code: 0xe818"><i class="demo-icon icon-pencil"></i> <span class="i-name">icon-pencil</span><span class="i-code">0xe818</span></div> <div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin"></i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div> <div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin"></i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div> <div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext"></i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div> - <div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt"></i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div> </div> <div class="row"> + <div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt"></i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div> <div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu"></i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div> <div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt"></i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div> <div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty"></i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div> - <div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared"></i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div> </div> <div class="row"> + <div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared"></i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div> <div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply"></i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div> <div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt"></i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div> <div class="the-icons span3" title="Code: 0xf144"><i class="demo-icon icon-play-circled"></i> <span class="i-name">icon-play-circled</span><span class="i-code">0xf144</span></div> - <div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt"></i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div> </div> <div class="row"> + <div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt"></i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div> <div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars"></i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div> <div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus"></i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div> </div> diff --git a/static/font/font/fontello.eot b/static/font/font/fontello.eot old mode 100644 new mode 100755 index 30867c92..a72671b0 Binary files a/static/font/font/fontello.eot and b/static/font/font/fontello.eot differ diff --git a/static/font/font/fontello.svg b/static/font/font/fontello.svg old mode 100644 new mode 100755 index b5a6725a..91aba5ef --- a/static/font/font/fontello.svg +++ b/static/font/font/fontello.svg @@ -54,6 +54,8 @@ <glyph glyph-name="edit" unicode="" d="M496 196l64 65-85 85-64-65v-31h53v-54h32z m245 402q-9 9-18 0l-196-196q-9-9 0-18t18 0l196 196q9 9 0 18z m45-331v-106q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-8-8-18-4-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v70q0 7 5 12l36 36q8 8 20 4t11-16z m-54 411l161-160-375-375h-161v160z m248-73l-51-52-161 161 51 52q16 15 38 15t38-15l85-85q16-16 16-38t-16-38z" horiz-adv-x="1000" /> +<glyph glyph-name="pencil" unicode="" d="M203 0l50 51-131 131-51-51v-60h72v-71h60z m291 518q0 12-12 12-5 0-9-4l-303-302q-4-4-4-10 0-12 13-12 5 0 9 4l303 302q3 4 3 10z m-30 107l232-232-464-465h-232v233z m381-54q0-29-20-50l-93-93-232 233 93 92q20 21 50 21 29 0 51-21l131-131q20-22 20-51z" horiz-adv-x="857.1" /> + <glyph glyph-name="spin3" unicode="" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" /> <glyph glyph-name="spin4" unicode="" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" /> diff --git a/static/font/font/fontello.ttf b/static/font/font/fontello.ttf old mode 100644 new mode 100755 index 87c3d0d9..9d36bc11 Binary files a/static/font/font/fontello.ttf and b/static/font/font/fontello.ttf differ diff --git a/static/font/font/fontello.woff b/static/font/font/fontello.woff old mode 100644 new mode 100755 index 3a87b4b6..35eea15d Binary files a/static/font/font/fontello.woff and b/static/font/font/fontello.woff differ diff --git a/static/font/font/fontello.woff2 b/static/font/font/fontello.woff2 old mode 100644 new mode 100755 index 009e5e85..c88c4b24 Binary files a/static/font/font/fontello.woff2 and b/static/font/font/fontello.woff2 differ diff --git a/test/unit/specs/components/user_profile.spec.js b/test/unit/specs/components/user_profile.spec.js index 41fd9cd0..847481f3 100644 --- a/test/unit/specs/components/user_profile.spec.js +++ b/test/unit/specs/components/user_profile.spec.js @@ -12,9 +12,13 @@ const mutations = { setError: () => {} } +const actions = { + fetchUser: () => {}, + fetchUserByScreenName: () => {} +} + const testGetters = { - userByName: state => getters.userByName(state.users), - userById: state => getters.userById(state.users) + findUser: state => getters.findUser(state.users) } const localUser = { @@ -31,6 +35,7 @@ const extUser = { const externalProfileStore = new Vuex.Store({ mutations, + actions, getters: testGetters, state: { api: { @@ -89,7 +94,7 @@ const externalProfileStore = new Vuex.Store({ currentUser: { credentials: '' }, - usersObject: [extUser], + usersObject: { 100: extUser }, users: [extUser] } } @@ -97,6 +102,7 @@ const externalProfileStore = new Vuex.Store({ const localProfileStore = new Vuex.Store({ mutations, + actions, getters: testGetters, state: { api: { @@ -155,7 +161,7 @@ const localProfileStore = new Vuex.Store({ currentUser: { credentials: '' }, - usersObject: [localUser], + usersObject: { 100: localUser, 'testuser': localUser }, users: [localUser] } } diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js index 864b798d..0bbcb25a 100644 --- a/test/unit/specs/modules/statuses.spec.js +++ b/test/unit/specs/modules/statuses.spec.js @@ -14,238 +14,258 @@ const makeMockStatus = ({id, text, type = 'status'}) => { } } -describe('Statuses.prepareStatus', () => { - it('sets deleted flag to false', () => { - const aStatus = makeMockStatus({id: '1', text: 'Hello oniichan'}) - expect(prepareStatus(aStatus).deleted).to.eq(false) - }) -}) - -describe('The Statuses module', () => { - it('adds the status to allStatuses and to the given timeline', () => { - const state = defaultState() - const status = makeMockStatus({id: '1'}) - - mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' }) - - expect(state.allStatuses).to.eql([status]) - expect(state.timelines.public.statuses).to.eql([status]) - expect(state.timelines.public.visibleStatuses).to.eql([]) - expect(state.timelines.public.newStatusCount).to.equal(1) +describe('Statuses module', () => { + describe('prepareStatus', () => { + it('sets deleted flag to false', () => { + const aStatus = makeMockStatus({id: '1', text: 'Hello oniichan'}) + expect(prepareStatus(aStatus).deleted).to.eq(false) + }) }) - it('counts the status as new if it has not been seen on this timeline', () => { - const state = defaultState() - const status = makeMockStatus({id: '1'}) + describe('addNewStatuses', () => { + it('adds the status to allStatuses and to the given timeline', () => { + const state = defaultState() + const status = makeMockStatus({id: '1'}) - mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' }) - mutations.addNewStatuses(state, { statuses: [status], timeline: 'friends' }) + mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' }) - expect(state.allStatuses).to.eql([status]) - expect(state.timelines.public.statuses).to.eql([status]) - expect(state.timelines.public.visibleStatuses).to.eql([]) - expect(state.timelines.public.newStatusCount).to.equal(1) + expect(state.allStatuses).to.eql([status]) + expect(state.timelines.public.statuses).to.eql([status]) + expect(state.timelines.public.visibleStatuses).to.eql([]) + expect(state.timelines.public.newStatusCount).to.equal(1) + }) - expect(state.allStatuses).to.eql([status]) - expect(state.timelines.friends.statuses).to.eql([status]) - expect(state.timelines.friends.visibleStatuses).to.eql([]) - expect(state.timelines.friends.newStatusCount).to.equal(1) + it('counts the status as new if it has not been seen on this timeline', () => { + const state = defaultState() + const status = makeMockStatus({id: '1'}) + + mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' }) + mutations.addNewStatuses(state, { statuses: [status], timeline: 'friends' }) + + expect(state.allStatuses).to.eql([status]) + expect(state.timelines.public.statuses).to.eql([status]) + expect(state.timelines.public.visibleStatuses).to.eql([]) + expect(state.timelines.public.newStatusCount).to.equal(1) + + expect(state.allStatuses).to.eql([status]) + expect(state.timelines.friends.statuses).to.eql([status]) + expect(state.timelines.friends.visibleStatuses).to.eql([]) + expect(state.timelines.friends.newStatusCount).to.equal(1) + }) + + it('add the statuses to allStatuses if no timeline is given', () => { + const state = defaultState() + const status = makeMockStatus({id: '1'}) + + mutations.addNewStatuses(state, { statuses: [status] }) + + expect(state.allStatuses).to.eql([status]) + expect(state.timelines.public.statuses).to.eql([]) + expect(state.timelines.public.visibleStatuses).to.eql([]) + expect(state.timelines.public.newStatusCount).to.equal(0) + }) + + it('adds the status to allStatuses and to the given timeline, directly visible', () => { + const state = defaultState() + const status = makeMockStatus({id: '1'}) + + mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) + + expect(state.allStatuses).to.eql([status]) + expect(state.timelines.public.statuses).to.eql([status]) + expect(state.timelines.public.visibleStatuses).to.eql([status]) + expect(state.timelines.public.newStatusCount).to.equal(0) + }) + + it('removes statuses by tag on deletion', () => { + const state = defaultState() + const status = makeMockStatus({id: '1'}) + const otherStatus = makeMockStatus({id: '3'}) + status.uri = 'xxx' + const deletion = makeMockStatus({id: '2', type: 'deletion'}) + deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.' + deletion.uri = 'xxx' + + mutations.addNewStatuses(state, { statuses: [status, otherStatus], showImmediately: true, timeline: 'public' }) + mutations.addNewStatuses(state, { statuses: [deletion], showImmediately: true, timeline: 'public' }) + + expect(state.allStatuses).to.eql([otherStatus]) + expect(state.timelines.public.statuses).to.eql([otherStatus]) + expect(state.timelines.public.visibleStatuses).to.eql([otherStatus]) + expect(state.timelines.public.maxId).to.eql('3') + }) + + it('does not update the maxId when the noIdUpdate flag is set', () => { + const state = defaultState() + const status = makeMockStatus({id: '1'}) + const secondStatus = makeMockStatus({id: '2'}) + + mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) + expect(state.timelines.public.maxId).to.eql('1') + + mutations.addNewStatuses(state, { statuses: [secondStatus], showImmediately: true, timeline: 'public', noIdUpdate: true }) + expect(state.timelines.public.statuses).to.eql([secondStatus, status]) + expect(state.timelines.public.visibleStatuses).to.eql([secondStatus, status]) + expect(state.timelines.public.maxId).to.eql('1') + }) + + it('keeps a descending by id order in timeline.visibleStatuses and timeline.statuses', () => { + const state = defaultState() + const nonVisibleStatus = makeMockStatus({id: '1'}) + const status = makeMockStatus({id: '3'}) + const statusTwo = makeMockStatus({id: '2'}) + const statusThree = makeMockStatus({id: '4'}) + + mutations.addNewStatuses(state, { statuses: [nonVisibleStatus], showImmediately: false, timeline: 'public' }) + + mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) + mutations.addNewStatuses(state, { statuses: [statusTwo], showImmediately: true, timeline: 'public' }) + + expect(state.timelines.public.minVisibleId).to.equal('2') + + mutations.addNewStatuses(state, { statuses: [statusThree], showImmediately: true, timeline: 'public' }) + + expect(state.timelines.public.statuses).to.eql([statusThree, status, statusTwo, nonVisibleStatus]) + expect(state.timelines.public.visibleStatuses).to.eql([statusThree, status, statusTwo]) + }) + + it('splits retweets from their status and links them', () => { + const state = defaultState() + const status = makeMockStatus({id: '1'}) + const retweet = makeMockStatus({id: '2', type: 'retweet'}) + const modStatus = makeMockStatus({id: '1', text: 'something else'}) + + retweet.retweeted_status = status + + // It adds both statuses, but only the retweet to visible. + mutations.addNewStatuses(state, { statuses: [retweet], timeline: 'public', showImmediately: true }) + expect(state.timelines.public.visibleStatuses).to.have.length(1) + expect(state.timelines.public.statuses).to.have.length(1) + expect(state.allStatuses).to.have.length(2) + expect(state.allStatuses[0].id).to.equal('1') + expect(state.allStatuses[1].id).to.equal('2') + + // It refers to the modified status. + mutations.addNewStatuses(state, { statuses: [modStatus], timeline: 'public' }) + expect(state.allStatuses).to.have.length(2) + expect(state.allStatuses[0].id).to.equal('1') + expect(state.allStatuses[0].text).to.equal(modStatus.text) + expect(state.allStatuses[1].id).to.equal('2') + expect(retweet.retweeted_status.text).to.eql(modStatus.text) + }) + + it('replaces existing statuses with the same id', () => { + const state = defaultState() + const status = makeMockStatus({id: '1'}) + const modStatus = makeMockStatus({id: '1', text: 'something else'}) + + // Add original status + mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) + expect(state.timelines.public.visibleStatuses).to.have.length(1) + expect(state.allStatuses).to.have.length(1) + + // Add new version of status + mutations.addNewStatuses(state, { statuses: [modStatus], showImmediately: true, timeline: 'public' }) + expect(state.timelines.public.visibleStatuses).to.have.length(1) + expect(state.allStatuses).to.have.length(1) + expect(state.allStatuses[0].text).to.eql(modStatus.text) + }) + + it('replaces existing statuses with the same id, coming from a retweet', () => { + const state = defaultState() + const status = makeMockStatus({id: '1'}) + const modStatus = makeMockStatus({id: '1', text: 'something else'}) + const retweet = makeMockStatus({id: '2', type: 'retweet'}) + retweet.retweeted_status = modStatus + + // Add original status + mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) + expect(state.timelines.public.visibleStatuses).to.have.length(1) + expect(state.allStatuses).to.have.length(1) + + // Add new version of status + mutations.addNewStatuses(state, { statuses: [retweet], showImmediately: false, timeline: 'public' }) + expect(state.timelines.public.visibleStatuses).to.have.length(1) + // Don't add the retweet itself if the tweet is visible + expect(state.timelines.public.statuses).to.have.length(1) + expect(state.allStatuses).to.have.length(2) + expect(state.allStatuses[0].text).to.eql(modStatus.text) + }) + + it('handles favorite actions', () => { + const state = defaultState() + const status = makeMockStatus({id: '1'}) + + const favorite = { + id: '2', + type: 'favorite', + in_reply_to_status_id: '1', // The API uses strings here... + uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00', + text: 'a favorited something by b', + user: { id: '99' } + } + + mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) + mutations.addNewStatuses(state, { statuses: [favorite], showImmediately: true, timeline: 'public' }) + + expect(state.timelines.public.visibleStatuses.length).to.eql(1) + expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1) + expect(state.timelines.public.maxId).to.eq(favorite.id) + + // Adding it again does nothing + mutations.addNewStatuses(state, { statuses: [favorite], showImmediately: true, timeline: 'public' }) + + expect(state.timelines.public.visibleStatuses.length).to.eql(1) + expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1) + expect(state.timelines.public.maxId).to.eq(favorite.id) + + // If something is favorited by the current user, it also sets the 'favorited' property but does not increment counter to avoid over-counting. Counter is incremented (updated, really) via response to the favorite request. + const user = { + id: '1' + } + + const ownFavorite = { + id: '3', + type: 'favorite', + in_reply_to_status_id: '1', // The API uses strings here... + uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00', + text: 'a favorited something by b', + user + } + + mutations.addNewStatuses(state, { statuses: [ownFavorite], showImmediately: true, timeline: 'public', user }) + + expect(state.timelines.public.visibleStatuses.length).to.eql(1) + expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1) + expect(state.timelines.public.visibleStatuses[0].favorited).to.eql(true) + }) }) - it('add the statuses to allStatuses if no timeline is given', () => { - const state = defaultState() - const status = makeMockStatus({id: '1'}) + describe('showNewStatuses', () => { + it('resets the minId to the min of the visible statuses when adding new to visible statuses', () => { + const state = defaultState() + const status = makeMockStatus({ id: '10' }) + mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) + const newStatus = makeMockStatus({ id: '20' }) + mutations.addNewStatuses(state, { statuses: [newStatus], showImmediately: false, timeline: 'public' }) + state.timelines.public.minId = '5' + mutations.showNewStatuses(state, { timeline: 'public' }) - mutations.addNewStatuses(state, { statuses: [status] }) - - expect(state.allStatuses).to.eql([status]) - expect(state.timelines.public.statuses).to.eql([]) - expect(state.timelines.public.visibleStatuses).to.eql([]) - expect(state.timelines.public.newStatusCount).to.equal(0) + expect(state.timelines.public.visibleStatuses.length).to.eql(2) + expect(state.timelines.public.minVisibleId).to.eql('10') + expect(state.timelines.public.minId).to.eql('10') + }) }) - it('adds the status to allStatuses and to the given timeline, directly visible', () => { - const state = defaultState() - const status = makeMockStatus({id: '1'}) + describe('clearTimeline', () => { + it('keeps userId when clearing user timeline', () => { + const state = defaultState() + state.timelines.user.userId = 123 - mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) + mutations.clearTimeline(state, { timeline: 'user' }) - expect(state.allStatuses).to.eql([status]) - expect(state.timelines.public.statuses).to.eql([status]) - expect(state.timelines.public.visibleStatuses).to.eql([status]) - expect(state.timelines.public.newStatusCount).to.equal(0) - }) - - it('removes statuses by tag on deletion', () => { - const state = defaultState() - const status = makeMockStatus({id: '1'}) - const otherStatus = makeMockStatus({id: '3'}) - status.uri = 'xxx' - const deletion = makeMockStatus({id: '2', type: 'deletion'}) - deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.' - deletion.uri = 'xxx' - - mutations.addNewStatuses(state, { statuses: [status, otherStatus], showImmediately: true, timeline: 'public' }) - mutations.addNewStatuses(state, { statuses: [deletion], showImmediately: true, timeline: 'public' }) - - expect(state.allStatuses).to.eql([otherStatus]) - expect(state.timelines.public.statuses).to.eql([otherStatus]) - expect(state.timelines.public.visibleStatuses).to.eql([otherStatus]) - expect(state.timelines.public.maxId).to.eql('3') - }) - - it('does not update the maxId when the noIdUpdate flag is set', () => { - const state = defaultState() - const status = makeMockStatus({id: '1'}) - const secondStatus = makeMockStatus({id: '2'}) - - mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) - expect(state.timelines.public.maxId).to.eql('1') - - mutations.addNewStatuses(state, { statuses: [secondStatus], showImmediately: true, timeline: 'public', noIdUpdate: true }) - expect(state.timelines.public.statuses).to.eql([secondStatus, status]) - expect(state.timelines.public.visibleStatuses).to.eql([secondStatus, status]) - expect(state.timelines.public.maxId).to.eql('1') - }) - - it('keeps a descending by id order in timeline.visibleStatuses and timeline.statuses', () => { - const state = defaultState() - const nonVisibleStatus = makeMockStatus({id: '1'}) - const status = makeMockStatus({id: '3'}) - const statusTwo = makeMockStatus({id: '2'}) - const statusThree = makeMockStatus({id: '4'}) - - mutations.addNewStatuses(state, { statuses: [nonVisibleStatus], showImmediately: false, timeline: 'public' }) - - mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) - mutations.addNewStatuses(state, { statuses: [statusTwo], showImmediately: true, timeline: 'public' }) - - expect(state.timelines.public.minVisibleId).to.equal('2') - - mutations.addNewStatuses(state, { statuses: [statusThree], showImmediately: true, timeline: 'public' }) - - expect(state.timelines.public.statuses).to.eql([statusThree, status, statusTwo, nonVisibleStatus]) - expect(state.timelines.public.visibleStatuses).to.eql([statusThree, status, statusTwo]) - }) - - it('splits retweets from their status and links them', () => { - const state = defaultState() - const status = makeMockStatus({id: '1'}) - const retweet = makeMockStatus({id: '2', type: 'retweet'}) - const modStatus = makeMockStatus({id: '1', text: 'something else'}) - - retweet.retweeted_status = status - - // It adds both statuses, but only the retweet to visible. - mutations.addNewStatuses(state, { statuses: [retweet], timeline: 'public', showImmediately: true }) - expect(state.timelines.public.visibleStatuses).to.have.length(1) - expect(state.timelines.public.statuses).to.have.length(1) - expect(state.allStatuses).to.have.length(2) - expect(state.allStatuses[0].id).to.equal('1') - expect(state.allStatuses[1].id).to.equal('2') - - // It refers to the modified status. - mutations.addNewStatuses(state, { statuses: [modStatus], timeline: 'public' }) - expect(state.allStatuses).to.have.length(2) - expect(state.allStatuses[0].id).to.equal('1') - expect(state.allStatuses[0].text).to.equal(modStatus.text) - expect(state.allStatuses[1].id).to.equal('2') - expect(retweet.retweeted_status.text).to.eql(modStatus.text) - }) - - it('replaces existing statuses with the same id', () => { - const state = defaultState() - const status = makeMockStatus({id: '1'}) - const modStatus = makeMockStatus({id: '1', text: 'something else'}) - - // Add original status - mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) - expect(state.timelines.public.visibleStatuses).to.have.length(1) - expect(state.allStatuses).to.have.length(1) - - // Add new version of status - mutations.addNewStatuses(state, { statuses: [modStatus], showImmediately: true, timeline: 'public' }) - expect(state.timelines.public.visibleStatuses).to.have.length(1) - expect(state.allStatuses).to.have.length(1) - expect(state.allStatuses[0].text).to.eql(modStatus.text) - }) - - it('replaces existing statuses with the same id, coming from a retweet', () => { - const state = defaultState() - const status = makeMockStatus({id: '1'}) - const modStatus = makeMockStatus({id: '1', text: 'something else'}) - const retweet = makeMockStatus({id: '2', type: 'retweet'}) - retweet.retweeted_status = modStatus - - // Add original status - mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) - expect(state.timelines.public.visibleStatuses).to.have.length(1) - expect(state.allStatuses).to.have.length(1) - - // Add new version of status - mutations.addNewStatuses(state, { statuses: [retweet], showImmediately: false, timeline: 'public' }) - expect(state.timelines.public.visibleStatuses).to.have.length(1) - // Don't add the retweet itself if the tweet is visible - expect(state.timelines.public.statuses).to.have.length(1) - expect(state.allStatuses).to.have.length(2) - expect(state.allStatuses[0].text).to.eql(modStatus.text) - }) - - it('handles favorite actions', () => { - const state = defaultState() - const status = makeMockStatus({id: '1'}) - - const favorite = { - id: '2', - type: 'favorite', - in_reply_to_status_id: '1', // The API uses strings here... - uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00', - text: 'a favorited something by b', - user: { id: '99' } - } - - mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) - mutations.addNewStatuses(state, { statuses: [favorite], showImmediately: true, timeline: 'public' }) - - expect(state.timelines.public.visibleStatuses.length).to.eql(1) - expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1) - expect(state.timelines.public.maxId).to.eq(favorite.id) - - // Adding it again does nothing - mutations.addNewStatuses(state, { statuses: [favorite], showImmediately: true, timeline: 'public' }) - - expect(state.timelines.public.visibleStatuses.length).to.eql(1) - expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1) - expect(state.timelines.public.maxId).to.eq(favorite.id) - - // If something is favorited by the current user, it also sets the 'favorited' property but does not increment counter to avoid over-counting. Counter is incremented (updated, really) via response to the favorite request. - const user = { - id: '1' - } - - const ownFavorite = { - id: '3', - type: 'favorite', - in_reply_to_status_id: '1', // The API uses strings here... - uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00', - text: 'a favorited something by b', - user - } - - mutations.addNewStatuses(state, { statuses: [ownFavorite], showImmediately: true, timeline: 'public', user }) - - expect(state.timelines.public.visibleStatuses.length).to.eql(1) - expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1) - expect(state.timelines.public.visibleStatuses[0].favorited).to.eql(true) - }) - - it('keeps userId when clearing user timeline', () => { - const state = defaultState() - state.timelines.user.userId = 123 - - mutations.clearTimeline(state, { timeline: 'user' }) - - expect(state.timelines.user.userId).to.eql(123) + expect(state.timelines.user.userId).to.eql(123) + }) }) describe('notifications', () => { diff --git a/test/unit/specs/modules/users.spec.js b/test/unit/specs/modules/users.spec.js index 4d49ee24..c8bc0ae7 100644 --- a/test/unit/specs/modules/users.spec.js +++ b/test/unit/specs/modules/users.spec.js @@ -34,40 +34,31 @@ describe('The users module', () => { }) }) - describe('getUserByName', () => { + describe('findUser', () => { it('returns user with matching screen_name', () => { + const user = { screen_name: 'Guy', id: '1' } const state = { - users: [ - { screen_name: 'Guy', id: '1' } - ] + usersObject: { + 1: user, + guy: user + } } const name = 'Guy' const expected = { screen_name: 'Guy', id: '1' } - expect(getters.userByName(state)(name)).to.eql(expected) + expect(getters.findUser(state)(name)).to.eql(expected) }) - it('returns user with matching screen_name with different case', () => { - const state = { - users: [ - { screen_name: 'guy', id: '1' } - ] - } - const name = 'Guy' - const expected = { screen_name: 'guy', id: '1' } - expect(getters.userByName(state)(name)).to.eql(expected) - }) - }) - - describe('getUserById', () => { it('returns user with matching id', () => { + const user = { screen_name: 'Guy', id: '1' } const state = { - users: [ - { screen_name: 'Guy', id: '1' } - ] + usersObject: { + 1: user, + guy: user + } } const id = '1' const expected = { screen_name: 'Guy', id: '1' } - expect(getters.userById(state)(id)).to.eql(expected) + expect(getters.findUser(state)(id)).to.eql(expected) }) }) }) diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js index 6245361c..2b0b0d6d 100644 --- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js +++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -1,4 +1,4 @@ -import { parseStatus, parseUser, parseNotification } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' +import { parseStatus, parseUser, parseNotification, addEmojis } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' import mastoapidata from '../../../../fixtures/mastoapi.json' import qvitterapidata from '../../../../fixtures/statuses.json' @@ -143,6 +143,23 @@ const makeMockNotificationQvitter = (overrides = {}) => { }, overrides) } +const makeMockEmojiMasto = (overrides = [{}]) => { + return [ + Object.assign({ + shortcode: 'image', + static_url: 'https://example.com/image.png', + url: 'https://example.com/image.png', + visible_in_picker: false + }, overrides[0]), + Object.assign({ + shortcode: 'thinking', + static_url: 'https://example.com/think.png', + url: 'https://example.com/think.png', + visible_in_picker: false + }, overrides[1]) + ] +} + parseNotification parseUser parseStatus @@ -218,6 +235,22 @@ describe('API Entities normalizer', () => { expect(parsedRepeat).to.have.property('retweeted_status') expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef') }) + + it('adds emojis to post content', () => { + const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), content: 'Makes you think :thinking:' }) + + const parsedPost = parseStatus(post) + + expect(parsedPost).to.have.property('statusnet_html').that.contains('<img') + }) + + it('adds emojis to subject line', () => { + const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), spoiler_text: 'CW: 300 IQ :thinking:' }) + + const parsedPost = parseStatus(post) + + expect(parsedPost).to.have.property('summary_html').that.contains('<img') + }) }) }) @@ -230,6 +263,22 @@ describe('API Entities normalizer', () => { expect(parseUser(local)).to.have.property('is_local', true) expect(parseUser(remote)).to.have.property('is_local', false) }) + + it('adds emojis to user name', () => { + const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), display_name: 'The :thinking: thinker' }) + + const parsedUser = parseUser(user) + + expect(parsedUser).to.have.property('name_html').that.contains('<img') + }) + + it('adds emojis to user bio', () => { + const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), note: 'Hello i like to :thinking: a lot' }) + + const parsedUser = parseUser(user) + + expect(parsedUser).to.have.property('description_html').that.contains('<img') + }) }) // We currently use QvitterAPI notifications only, and especially due to MastoAPI lacking is_seen, support for MastoAPI @@ -267,4 +316,28 @@ describe('API Entities normalizer', () => { expect(parseNotification(notif)).to.have.deep.property('from_profile.id', 'spurdo') }) }) + + describe('MastoAPI emoji adder', () => { + const emojis = makeMockEmojiMasto() + const imageHtml = '<img src="https://example.com/image.png" alt="image" class="emoji" />' + .replace(/"/g, '\'') + const thinkHtml = '<img src="https://example.com/think.png" alt="thinking" class="emoji" />' + .replace(/"/g, '\'') + + it('correctly replaces shortcodes in supplied string', () => { + const result = addEmojis('This post has :image: emoji and :thinking: emoji', emojis) + expect(result).to.include(thinkHtml) + expect(result).to.include(imageHtml) + }) + + it('handles consecutive emojis correctly', () => { + const result = addEmojis('Lelel emoji spam :thinking::thinking::thinking::thinking:', emojis) + expect(result).to.include(thinkHtml + thinkHtml + thinkHtml + thinkHtml) + }) + + it('Doesn\'t replace nonexistent emojis', () => { + const result = addEmojis('Admin add the :tenshi: emoji', emojis) + expect(result).to.equal('Admin add the :tenshi: emoji') + }) + }) })