aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.js4
-rw-r--r--src/App.scss25
-rw-r--r--src/App.vue1
-rw-r--r--src/boot/after_store.js341
-rw-r--r--src/boot/routes.js2
-rw-r--r--src/components/block_card/block_card.js2
-rw-r--r--src/components/media_modal/media_modal.vue15
-rw-r--r--src/components/mobile_post_status_modal/mobile_post_status_modal.js91
-rw-r--r--src/components/mobile_post_status_modal/mobile_post_status_modal.vue76
-rw-r--r--src/components/mute_card/mute_card.js2
-rw-r--r--src/components/post_status_form/post_status_form.js3
-rw-r--r--src/components/post_status_form/post_status_form.vue1
-rw-r--r--src/components/side_drawer/side_drawer.vue15
-rw-r--r--src/components/status/status.js8
-rw-r--r--src/components/user_avatar/user_avatar.js5
-rw-r--r--src/components/user_card/user_card.js3
-rw-r--r--src/components/user_card/user_card.vue14
-rw-r--r--src/components/user_profile/user_profile.js94
-rw-r--r--src/components/user_profile/user_profile.vue4
-rw-r--r--src/i18n/ar.json2
-rw-r--r--src/i18n/ca.json2
-rw-r--r--src/i18n/cs.json15
-rw-r--r--src/i18n/de.json2
-rw-r--r--src/i18n/eo.json2
-rw-r--r--src/i18n/es.json2
-rw-r--r--src/i18n/fi.json2
-rw-r--r--src/i18n/fr.json2
-rw-r--r--src/i18n/ga.json2
-rw-r--r--src/i18n/he.json2
-rw-r--r--src/i18n/it.json2
-rw-r--r--src/i18n/ja.json2
-rw-r--r--src/i18n/ko.json2
-rw-r--r--src/i18n/nb.json2
-rw-r--r--src/i18n/nl.json2
-rw-r--r--src/i18n/oc.json2
-rw-r--r--src/i18n/pt.json2
-rw-r--r--src/i18n/zh.json2
-rw-r--r--src/lib/persisted_state.js12
-rw-r--r--src/main.js7
-rw-r--r--src/modules/chat.js10
-rw-r--r--src/modules/statuses.js7
-rw-r--r--src/modules/users.js40
-rw-r--r--src/services/api/api.service.js33
-rw-r--r--src/services/backend_interactor_service/backend_interactor_service.js5
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js31
45 files changed, 619 insertions, 281 deletions
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..598735d9 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -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..0a4ec857 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
-
- 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
-
- // 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 copyInstanceOption = (name) => {
- store.dispatch('setInstanceOption', {name, value: config[name]})
- }
-
- copyInstanceOption('nsfwCensorImage')
- copyInstanceOption('background')
- copyInstanceOption('hidePostStats')
- copyInstanceOption('hideUserStats')
- copyInstanceOption('hideFilteredStatuses')
- copyInstanceOption('logo')
-
- store.dispatch('setInstanceOption', {
- name: 'logoMask',
- value: typeof config.logoMask === 'undefined'
- ? true
- : config.logoMask
- })
-
- store.dispatch('setInstanceOption', {
- name: 'logoMargin',
- value: typeof config.logoMargin === 'undefined'
- ? 0
- : config.logoMargin
- })
-
- 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')
-
- if ((config.chatDisabled)) {
- store.dispatch('disableChat')
- } else {
- store.dispatch('initializeSocket')
- }
-
- 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 }
- }
- })
-
- /* eslint-disable no-new */
- new Vue({
- router,
- store,
- i18n,
- el: '#app',
- render: h => h(App)
- })
- })
- })
+ return data.site.pleromafe
+ } else {
+ throw (res)
+ }
+ } catch (error) {
+ console.error('Could not load statusnet config, potentially fatal')
+ console.error(error)
+ }
+}
+
+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 {}
+ }
+}
+
+const setSettings = async ({ apiConfig, staticConfig, store }) => {
+ const overrides = window.___pleromafe_dev_overrides || {}
+ const env = window.___pleromafe_mode.NODE_ENV
+
+ // 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 copyInstanceOption = (name) => {
+ store.dispatch('setInstanceOption', { name, value: config[name] })
+ }
+
+ copyInstanceOption('nsfwCensorImage')
+ copyInstanceOption('background')
+ copyInstanceOption('hidePostStats')
+ copyInstanceOption('hideUserStats')
+ copyInstanceOption('hideFilteredStatuses')
+ copyInstanceOption('logo')
+
+ store.dispatch('setInstanceOption', {
+ name: 'logoMask',
+ value: typeof config.logoMask === 'undefined'
+ ? true
+ : config.logoMask
+ })
- window.fetch('/static/terms-of-service.html')
- .then((res) => res.text())
- .then((html) => {
+ store.dispatch('setInstanceOption', {
+ name: 'logoMargin',
+ value: typeof config.logoMargin === 'undefined'
+ ? 0
+ : config.logoMargin
+ })
+
+ 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')
+
+ if ((config.chatDisabled)) {
+ store.dispatch('disableChat')
+ } else {
+ store.dispatch('initializeSocket')
+ }
+
+ return store.dispatch('setTheme', config['theme'])
+}
+
+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)
+ }
+}
+
+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('/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)
- )
-
- 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,65 @@ 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 })
+ } 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/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/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..43a77f45 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -15,6 +15,9 @@ export default {
betterShadow: this.$store.state.interface.browserSupport.cssFilter
}
},
+ created () {
+ this.$store.dispatch('fetchUserRelationship', this.user.id)
+ },
computed: {
classes () {
return [{
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index cc2ce6b8..690e1bde 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>
@@ -159,6 +159,18 @@
&-bio {
text-align: center;
+
+ img {
+ object-fit: contain;
+ vertical-align: middle;
+ max-width: 100%;
+ max-height: 400px;
+
+ .emoji {
+ width: 32px;
+ height: 32px;
+ }
+ }
}
// Modifiers
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/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/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..baac3d25 100644
--- a/src/i18n/oc.json
+++ b/src/i18n/oc.json
@@ -71,7 +71,7 @@
"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"
},
"content_warning": "Avís de contengut (opcional)",
"default": "Escrivètz aquí vòstre estatut.",
diff --git a/src/i18n/pt.json b/src/i18n/pt.json
index cbc2c9a3..29ab995b 100644
--- a/src/i18n/pt.json
+++ b/src/i18n/pt.json
@@ -71,7 +71,7 @@
"account_not_locked_warning_link": "fechada",
"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!",
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/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 2de87026..1c6703b7 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -28,12 +28,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 DENY_USER_URL = '/api/pleroma/friendships/deny'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
+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`
}
@@ -588,6 +604,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 b7e5711e..02130dcf 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 = addEmojis(data.display_name, data.emojis)
+ // output.name = ??? missing
+ output.name_html = data.display_name
- output.description = null // missing
- output.description_html = addEmojis(data.note, data.emojis)
+ // output.description = ??? missing
+ output.description_html = data.note
// 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
@@ -174,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
@@ -187,8 +191,7 @@ export const parseStatus = (data) => {
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
@@ -267,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)