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 @@ + 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 @@ {{ isMe ? $t('user_card.its_you') : $t('user_card.follows_you') }} +
+ +
+
@@ -36,7 +37,11 @@ } &-buttons-wrapper { - margin-top: 15px; + margin-top: 10px; + + button { + margin-top: 5px; + } } } 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 @@