diff options
| author | Roger Braun <roger@rogerbraun.net> | 2017-03-09 09:19:40 +0100 |
|---|---|---|
| committer | Roger Braun <roger@rogerbraun.net> | 2017-03-09 09:19:40 +0100 |
| commit | 2f52669380703030aa8e197deb05240c5f3bf979 (patch) | |
| tree | 5b40b0d4efd3d132c8bdd321092ce2bd5fb8a0e6 /src | |
| parent | c921662036fe39f5146d5a36b4b018af3cfc2635 (diff) | |
| parent | 64d71b75cfed41d82a99e3e478fc75d5f81f84bd (diff) | |
Merge branch 'develop' into wakarimasen/pleroma-fe-develop
Diffstat (limited to 'src')
24 files changed, 329 insertions, 160 deletions
diff --git a/src/App.scss b/src/App.scss index 0945c76b..8a1942c6 100644 --- a/src/App.scss +++ b/src/App.scss @@ -109,8 +109,8 @@ main-router { .panel-heading { border-radius: 10px 10px 0 0; background-size: cover; - padding: 0.6em 0; - text-align: center; + padding: 0.6em 1.0em; + text-align: left; font-size: 1.3em; line-height: 24px; } diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index c3f52f57..7715add5 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -11,7 +11,9 @@ const Attachment = { return { nsfwImage, hideNsfwLocal: this.$store.state.config.hideNsfw, - showHidden: false + showHidden: false, + loading: false, + img: document.createElement('img') } }, computed: { @@ -20,6 +22,13 @@ const Attachment = { }, hidden () { return this.nsfw && this.hideNsfwLocal && !this.showHidden + }, + autoHeight () { + if (this.type === 'image' && this.nsfw) { + return { + 'min-height': '311px' + } + } } }, methods: { @@ -29,10 +38,15 @@ const Attachment = { } }, toggleHidden () { - let img = document.createElement('img') - img.src = this.attachment.url - img.onload = () => { - this.showHidden = !this.showHidden + if (this.img.onload) { + this.img.onload() + } else { + this.loading = true + this.img.src = this.attachment.url + this.img.onload = () => { + this.loading = false + this.showHidden = !this.showHidden + } } } } diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index ad60acf9..8f51b891 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -1,15 +1,14 @@ <template> - <div class="attachment" :class="type"> - <a class="image-attachment" v-if="hidden" v-on:click.prevent="toggleHidden()"> - <img :key="nsfwImage" :src="nsfwImage"></img> + <div class="attachment" :class="{[type]: true, loading}" :style="autoHeight"> + <a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()"> + <img :key="nsfwImage" :src="nsfwImage"/> </a> <div class="hider" v-if="nsfw && hideNsfwLocal && !hidden"> <a href="#" @click.prevent="toggleHidden()">Hide</a> </div> - <a class="image-attachment" v-if="type === 'image' && !hidden" - :href="attachment.url" target="_blank"> - <img class="base05-border" referrerpolicy="no-referrer" :src="attachment.large_thumb_url || attachment.url"></img> + <a v-if="type === 'image' && !hidden" class="image-attachment" :href="attachment.url" target="_blank"> + <img class="base05-border" referrerpolicy="no-referrer" :src="attachment.large_thumb_url || attachment.url"/> </a> <video v-if="type === 'video' && !hidden" :src="attachment.url" controls></video> @@ -18,7 +17,7 @@ <div @click.prevent="linkClicked" v-if="type === 'html' && attachment.oembed" class="oembed"> <div v-if="attachment.thumb_url" class="image"> - <img :src="attachment.thumb_url"></img> + <img :src="attachment.thumb_url"/> </div> <div class="text"> <h1><a :href="attachment.url">{{attachment.oembed.title}}</a></h1> @@ -45,6 +44,10 @@ display: flex; } + &.loading { + cursor: progress; + } + .hider { position: absolute; margin: 10px; @@ -111,7 +114,6 @@ flex: 1; img { - width: 100%; border-style: solid; border-width: 1px; border-radius: 5px; diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index ecc76e71..281b0183 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -47,6 +47,13 @@ const conversation = { .then((status) => this.$store.dispatch('addNewStatuses', { statuses: [status] })) .then(() => this.fetchConversation()) } + }, + focused: function (id) { + if (this.statusoid.retweeted_status) { + return (id === this.statusoid.retweeted_status.id) + } else { + return (id === this.statusoid.id) + } } } } diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 8e24fcbe..331cce99 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -2,13 +2,13 @@ <div class="timeline panel panel-default base00-background"> <div class="panel-heading base01-background base04"> Conversation - <div v-if="collapsable"> + <span v-if="collapsable" style="float:right;"> <small><a href="#" @click.prevent="$emit('toggleExpanded')">Collapse</a></small> - </div> + </span> </div> <div class="panel-body"> <div class="timeline"> - <status v-for="status in conversation" :key="status.id" v-bind:statusoid="status":expandable='false':inConversation='true'></status> + <status v-for="status in conversation" :key="status.id" :statusoid="status" :expandable='false' :focused="focused(status.id)" :inConversation='true'></status> </div> </div> </div> diff --git a/src/components/favorite_button/favorite_button.js b/src/components/favorite_button/favorite_button.js index 4ee3890f..466e9b84 100644 --- a/src/components/favorite_button/favorite_button.js +++ b/src/components/favorite_button/favorite_button.js @@ -1,5 +1,10 @@ const FavoriteButton = { - props: [ 'status' ], + props: ['status'], + data () { + return { + animated: false + } + }, methods: { favorite () { if (!this.status.favorited) { @@ -7,13 +12,18 @@ const FavoriteButton = { } else { this.$store.dispatch('unfavorite', {id: this.status.id}) } + this.animated = true + setTimeout(() => { + this.animated = false + }, 500) } }, computed: { classes () { return { 'icon-star-empty': !this.status.favorited, - 'icon-star': this.status.favorited + 'icon-star': this.status.favorited, + 'animate-spin': this.animated } } } diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue index fd53b505..0abece31 100644 --- a/src/components/favorite_button/favorite_button.vue +++ b/src/components/favorite_button/favorite_button.vue @@ -1,6 +1,6 @@ <template> <div> - <i :class='classes' class='favorite-button fa' v-on:click.prevent='favorite()'></i> + <i :class='classes' class='favorite-button fa' @click.prevent='favorite()'/> <span v-if='status.fave_num > 0'>{{status.fave_num}}</span> </div> </template> @@ -10,6 +10,7 @@ <style lang='scss'> .favorite-button { cursor: pointer; + animation-duration: 0.6s; &:hover { color: orange; } @@ -17,4 +18,5 @@ .icon-star { color: orange; } + </style> diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js index b55f770f..1a6f6015 100644 --- a/src/components/login_form/login_form.js +++ b/src/components/login_form/login_form.js @@ -1,15 +1,21 @@ const LoginForm = { data: () => ({ - user: {} + user: {}, + authError: false }), computed: { loggingIn () { return this.$store.state.users.loggingIn } }, methods: { submit () { - this.$store.dispatch('loginUser', this.user).then(() => { - this.$router.push('/main/friends') - }) + this.$store.dispatch('loginUser', this.user).then( + () => {}, + (error) => { + this.authError = error + this.user.username = '' + this.user.password = '' + } + ) } } } diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue index c0ea4313..b2fa5341 100644 --- a/src/components/login_form/login_form.vue +++ b/src/components/login_form/login_form.vue @@ -1,7 +1,7 @@ <template> <div class="login panel panel-default base00-background"> <!-- Default panel contents --> - <div class="panel-heading base01-background"> + <div class="panel-heading base01-background base04"> Log in </div> <div class="panel-body"> @@ -17,6 +17,9 @@ <div class='form-group'> <button :disabled="loggingIn" type='submit' class='btn btn-default base05 base01-background'>Submit</button> </div> + <div v-if="authError" class='form-group'> + <div class='error base05'>{{authError}}</div> + </div> </form> </div> </div> @@ -39,6 +42,14 @@ margin-top: 1.0em; min-height: 28px; } + + .error { + border-radius: 5px; + text-align: center; + background-color: rgba(255, 48, 16, 0.65); + min-height: 28px; + line-height: 28px; + } } </style> diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 85ed163c..1d96f4d6 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -1,24 +1,24 @@ <template> <div class="nav-panel"> - <div class="panel panel-default base01-background"> + <div class="panel panel-default base02-background"> <ul> <li v-if='currentUser'> - <router-link to='/main/friends'> + <router-link class="base01-background" to='/main/friends'> Timeline </router-link> </li> <li v-if='currentUser'> - <router-link :to="{ name: 'mentions', params: { username: currentUser.screen_name } }"> + <router-link class="base01-background" :to="{ name: 'mentions', params: { username: currentUser.screen_name } }"> Mentions </router-link> </li> <li> - <router-link to='/main/public'> + <router-link class="base01-background" to='/main/public'> Public Timeline </router-link> </li> <li> - <router-link to='/main/all'> + <router-link class="base01-background" to='/main/all'> The Whole Known Network </router-link> </li> @@ -30,7 +30,6 @@ <script src="./nav_panel.js" ></script> <style lang="scss"> - .nav-panel ul { list-style: none; margin: 0; @@ -39,7 +38,15 @@ .nav-panel li { border-bottom: 1px solid; - padding: 0.8em 0.85em; + padding: 0; + &:first-child a { + border-top-right-radius: 10px; + border-top-left-radius: 10px; + } + &:last-child a { + border-bottom-right-radius: 10px; + border-bottom-left-radius: 10px; + } } .nav-panel li:last-child { @@ -48,10 +55,16 @@ .nav-panel a { display: block; - width: 100%; - + padding: 0.8em 0.85em; + &:hover { + background-color: transparent; + } &.router-link-active { - font-weight: bold + font-weight: bolder; + background-color: transparent; + &:hover { + text-decoration: underline; + } } } diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index 100349eb..6ad7ec1e 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -6,13 +6,26 @@ // force the text to stay centered, while keeping // the button in the right side of the panel heading position: relative; - button { + .read-button { position: absolute; padding: 0.1em 0.3em 0.25em 0.3em; - right: 0.6em; + right: 0.7em; } } + .unseen-count { + display: inline-block; + background-color: rgba(255, 16, 8, 0.8); + text-shadow: 0px 0px 3px rgba(0, 0, 0, 0.5); + min-width: 1.3em; + border-radius: 1.3em; + margin: 0 0.2em 0 -0.4em; + color: white; + font-size: 0.9em; + text-align: center; + line-height: 1.3em; + } + .notification { // Will have to use pixels here to ensure consistent distance with // pad alone and pad + border, browsers bad at rounding this with em, @@ -64,7 +77,7 @@ } .unseen { - border-left: 4px solid rgba(255, 48, 16, 0.65); + border-left: 4px solid rgba(255, 16, 8, 0.75); padding-left: 6px; } } diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue index 41c274aa..91f6cfdc 100644 --- a/src/components/notifications/notifications.vue +++ b/src/components/notifications/notifications.vue @@ -2,8 +2,9 @@ <div class="notifications"> <div class="panel panel-default base00-background"> <div class="panel-heading base01-background base04"> - Notifications ({{unseenCount}}) - <button @click.prevent="markAsSeen" class="base05 base02-background">Read!</button> + <span class="unseen-count" v-if="unseenCount">{{unseenCount}}</span> + Notifications + <button @click.prevent="markAsSeen" class="base06 base02-background read-button">Read!</button> </div> <div class="panel-body"> <div v-for="notification in visibleNotifications" class="notification" :class='{"unseen": !notification.seen}'> diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js index e7318dc5..2280f315 100644 --- a/src/components/retweet_button/retweet_button.js +++ b/src/components/retweet_button/retweet_button.js @@ -1,16 +1,26 @@ const RetweetButton = { - props: [ 'status' ], + props: ['status'], + data () { + return { + animated: false + } + }, methods: { retweet () { if (!this.status.repeated) { this.$store.dispatch('retweet', {id: this.status.id}) } + this.animated = true + setTimeout(() => { + this.animated = false + }, 500) } }, computed: { classes () { return { - 'retweeted': this.status.repeated + 'retweeted': this.status.repeated, + 'animate-spin': this.animated } } } diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue index 9b2f5c7b..d923c5c4 100644 --- a/src/components/retweet_button/retweet_button.vue +++ b/src/components/retweet_button/retweet_button.vue @@ -11,12 +11,12 @@ @import '../../_variables.scss'; .icon-retweet { cursor: pointer; + animation-duration: 0.6s; &:hover { color: $green; } } .retweeted { - cursor: auto; color: $green; } </style> diff --git a/src/components/status/status.js b/src/components/status/status.js index 0783604d..3f34af36 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -9,7 +9,8 @@ const Status = { props: [ 'statusoid', 'expandable', - 'inConversation' + 'inConversation', + 'focused' ], data: () => ({ replying: false, @@ -34,7 +35,8 @@ const Status = { loggedIn () { return !!this.$store.state.users.currentUser }, - muted () { return !this.unmuted && this.status.user.muted } + muted () { return !this.unmuted && this.status.user.muted }, + isReply () { return !!this.status.in_reply_to_status_id } }, components: { Attachment, diff --git a/src/components/status/status.vue b/src/components/status/status.vue index d8dce423..a5abbf6b 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -1,5 +1,5 @@ <template> - <div class="status-el base00-background" v-if="!status.deleted" v-bind:class="{ 'status-conversation': inConversation }"> + <div class="status-el base00-background" v-if="!status.deleted" v-bind:class="[{ 'expanded-status': !expandable }, { 'base01-background': focused }, { 'status-conversation': inConversation }]"> <template v-if="muted"> <div class="media status container muted"> <small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small> @@ -34,6 +34,13 @@ {{status.in_reply_to_screen_name}} </router-link> </small> + <template v-if="isReply"> + <small> + <router-link :to="{ name: 'conversation', params: { id: status.in_reply_to_status_id } }"> + <i class="icon-reply"></i> + </router-link> + </small> + </template> - <small> <router-link :to="{ name: 'conversation', params: { id: status.id } }"> diff --git a/src/components/status_or_conversation/status_or_conversation.vue b/src/components/status_or_conversation/status_or_conversation.vue index 1e21bdcd..9647d5eb 100644 --- a/src/components/status_or_conversation/status_or_conversation.vue +++ b/src/components/status_or_conversation/status_or_conversation.vue @@ -1,7 +1,7 @@ <template> <div> <conversation v-if="expanded" @toggleExpanded="toggleExpanded" :collapsable="true" :statusoid="statusoid"></conversation> - <status v-if="!expanded" @toggleExpanded="toggleExpanded" :expandable="true" :inConversation="false" :statusoid="statusoid"></status> + <status v-if="!expanded" @toggleExpanded="toggleExpanded" :expandable="true" :inConversation="false" :focused="false" :statusoid="statusoid"></status> </div> </template> diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 15ac5ff8..ac074f3c 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -1,23 +1,26 @@ <template> <div class="timeline panel panel-default"> - <div class="panel-heading base01-background base04">{{title}}</div> + <div class="panel-heading timeline-heading base01-background base04"> + <div class="title"> + {{title}} + </div> + <button @click.prevent="showNewStatuses" class="base06 base02-background loadmore-button" v-if="timeline.newStatusCount > 0 && !timeline.error"> + Show new ({{timeline.newStatusCount}}) + </button> + <button @click.prevent class="base06 error no-press loadmore-button" v-if="timeline.error"> + Error fetching updates + </button> + <button @click.prevent class="base04 base01-background no-press loadmore-button" v-if="!timeline.newStatusCount > 0 && !timeline.error"> + Up-to-date + </button> + </div> <div class="panel-body"> <div class="timeline"> - <a href="#" v-on:click.prevent='showNewStatuses()' v-if="timeline.newStatusCount > 0"> - <div class="base01-background base05-border new-status-notification"> - <p class="text-center" > - {{timeline.newStatusCount}} new statuses - </p> - </div> - </a> <status-or-conversation v-for="status in timeline.visibleStatuses" :key="status.id" v-bind:statusoid="status"></status-or-conversation> <a href="#" v-on:click.prevent='fetchOlderStatuses()' v-if="!timeline.loading"> - <div class="base01-background base05-border new-status-notification"> - <p class="text-center" > - Load older statuses. - </p> - </div> + <div class="base01-background base05-border new-status-notification text-center">Load older statuses.</div> </a> + <div class="base01-background base05-border new-status-notification text-center" v-else>...</div> </div> </div> </div> @@ -25,14 +28,42 @@ <script src="./timeline.js"></script> <style lang="scss"> - .new-status-notification { - border-style: solid; - border-width: 1px 0 1px 0; - font-size: 1.1em; - p { - margin: 0px; - padding: 10px; - } - } + .timeline { + .timeline-heading { + position: relative; + display: flex; + } + .title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 70%; + } + .loadmore-button { + position: absolute; + right: 0.6em; + padding: 0.1em 0.3em 0.25em 0.3em; + min-width: 6em; + } + .error { + background-color: rgba(255, 48, 16, 0.65); + } + .no-press { + opacity: 0.8; + cursor: default; + } + } + + + .new-status-notification { + position:relative; + margin-top: -1px; + font-size: 1.1em; + border-width: 1px 0 0 0; + border-style: solid; + border-radius: 0 0 10px 10px; + padding: 10px; + z-index: 1; + } </style> diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue index 87a445ca..3fa34773 100644 --- a/src/components/user_card_content/user_card_content.vue +++ b/src/components/user_card_content/user_card_content.vue @@ -104,6 +104,11 @@ .profile-panel-background { background-size: cover; border-radius: 10px; + + .panel-heading { + padding: 0.6em 0em; + text-align: center; + } } .profile-panel-body { diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index 6a17ca99..a47ad7d5 100644 --- a/src/lib/persisted_state.js +++ b/src/lib/persisted_state.js @@ -1,7 +1,7 @@ import merge from 'lodash.merge' import objectPath from 'object-path' -import { throttle } from 'lodash' -import lzstring from 'lz-string' +import localforage from 'localforage' +import { throttle, each } from 'lodash' const defaultReducer = (state, paths) => ( paths.length === 0 ? state : paths.reduce((substate, path) => { @@ -11,32 +11,11 @@ const defaultReducer = (state, paths) => ( ) const defaultStorage = (() => { - const hasLocalStorage = typeof window !== 'undefined' && window.localStorage - if (hasLocalStorage) { - return window.localStorage - } - - class InternalStorage { - setItem (key, item) { - this[key] = item - return item - } - getItem (key) { - return this[key] - } - removeItem (key) { - delete this[key] - } - clear () { - Object.keys(this).forEach(key => delete this[key]) - } - } - - return new InternalStorage() + return localforage })() const defaultSetState = (key, state, storage) => { - return storage.setItem(key, lzstring.compressToUTF16(JSON.stringify(state))) + return storage.setItem(key, state) } export default function createPersistedState ({ @@ -44,12 +23,7 @@ export default function createPersistedState ({ paths = [], getState = (key, storage) => { let value = storage.getItem(key) - try { - value = lzstring.decompressFromUTF16(value) // inflate(value, { to: 'string' }) - } catch (e) { - console.log("Couldn't inflate value... Maybe upgrading") - } - return value && value !== 'undefined' ? JSON.parse(value) : undefined + return value }, setState = throttle(defaultSetState, 60000), reducer = defaultReducer, @@ -57,12 +31,20 @@ export default function createPersistedState ({ subscriber = store => handler => store.subscribe(handler) } = {}) { return store => { - const savedState = getState(key, storage) - if (typeof savedState === 'object') { - store.replaceState( - merge({}, store.state, savedState) - ) - } + getState(key, storage).then((savedState) => { + if (typeof savedState === 'object') { + // build user cache + const usersState = savedState.users || {} + usersState.usersObject = {} + const users = usersState.users || [] + each(users, (user) => { usersState.usersObject[user.id] = user }) + savedState.users = usersState + + store.replaceState( + merge({}, store.state, savedState) + ) + } + }) subscriber(store)((mutation, state) => { try { diff --git a/src/main.js b/src/main.js index b5c148ff..ab0fd6c0 100644 --- a/src/main.js +++ b/src/main.js @@ -54,7 +54,7 @@ const routes = [ { path: '/main/all', component: PublicAndExternalTimeline }, { path: '/main/public', component: PublicTimeline }, { path: '/main/friends', component: FriendsTimeline }, - { name: 'conversation', path: '/notice/:id', component: ConversationPage }, + { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, { name: 'user-profile', path: '/users/:id', component: UserProfile }, { name: 'mentions', path: '/:username/mentions', component: Mentions }, { name: 'settings', path: '/settings', component: Settings } @@ -64,6 +64,9 @@ const router = new VueRouter({ mode: 'history', routes, scrollBehavior: (to, from, savedPosition) => { + if (to.matched.some(m => m.meta.dontScroll)) { + return false + } return savedPosition || { x: 0, y: 0 } } }) diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 491d0024..e4528520 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -4,45 +4,58 @@ import apiService from '../services/api/api.service.js' export const defaultState = { allStatuses: [], + allStatusesObject: {}, maxId: 0, notifications: [], favorites: new Set(), timelines: { mentions: { statuses: [], + statusesObject: {}, faves: [], visibleStatuses: [], + visibleStatusesObject: {}, newStatusCount: 0, maxId: 0, minVisibleId: 0, - loading: false + loading: false, + error: false }, public: { statuses: [], + statusesObject: {}, faves: [], visibleStatuses: [], + visibleStatusesObject: {}, newStatusCount: 0, maxId: 0, minVisibleId: 0, - loading: false + loading: false, + error: false }, publicAndExternal: { statuses: [], + statusesObject: {}, faves: [], visibleStatuses: [], + visibleStatusesObject: {}, newStatusCount: 0, maxId: 0, minVisibleId: 0, - loading: false + loading: false, + error: false }, friends: { statuses: [], + statusesObject: {}, faves: [], visibleStatuses: [], + visibleStatusesObject: {}, newStatusCount: 0, maxId: 0, minVisibleId: 0, - loading: false + loading: false, + error: false } } } @@ -87,8 +100,9 @@ export const findMaxId = (...args) => { return (maxBy(flatten(args), 'id') || {}).id } -const mergeOrAdd = (arr, item) => { - const oldItem = find(arr, {id: item.id}) +const mergeOrAdd = (arr, obj, item) => { + const oldItem = obj[item.id] + if (oldItem) { // We already have this, so only merge the new info. merge(oldItem, item) @@ -99,6 +113,7 @@ const mergeOrAdd = (arr, item) => { // This is a new item, prepare it prepareStatus(item) arr.push(item) + obj[item.id] = item return {item, new: true} } } @@ -118,6 +133,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us } const allStatuses = state.allStatuses + const allStatusesObject = state.allStatusesObject const timelineObject = state.timelines[timeline] // Set the maxId to the new id if it's larger. @@ -127,7 +143,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us } const addStatus = (status, showImmediately, addToTimeline = true) => { - const result = mergeOrAdd(allStatuses, status) + const result = mergeOrAdd(allStatuses, allStatusesObject, status) status = result.item if (result.new) { @@ -143,7 +159,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us // Add the mention to the mentions timeline if (timelineObject !== mentions) { - mergeOrAdd(mentions.statuses, status) + mergeOrAdd(mentions.statuses, mentions.statusesObject, status) mentions.newStatusCount += 1 sortTimeline(mentions) @@ -157,13 +173,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us let resultForCurrentTimeline // Some statuses should only be added to the global status repository. if (timeline && addToTimeline) { - resultForCurrentTimeline = mergeOrAdd(timelineObject.statuses, status) + resultForCurrentTimeline = mergeOrAdd(timelineObject.statuses, timelineObject.statusesObject, status) } if (timeline && showImmediately) { // Add it directly to the visibleStatuses, don't change // newStatusCount - mergeOrAdd(timelineObject.visibleStatuses, status) + mergeOrAdd(timelineObject.visibleStatuses, timelineObject.visibleStatusesObject, status) } else if (timeline && addToTimeline && resultForCurrentTimeline.new) { // Just change newStatuscount timelineObject.newStatusCount += 1 @@ -260,26 +276,31 @@ export const mutations = { oldTimeline.newStatusCount = 0 oldTimeline.visibleStatuses = slice(oldTimeline.statuses, 0, 50) + oldTimeline.visibleStatusesObject = {} + each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status }) }, setFavorited (state, { status, value }) { - const newStatus = find(state.allStatuses, status) + const newStatus = state.allStatusesObject[status.id] newStatus.favorited = value }, setRetweeted (state, { status, value }) { - const newStatus = find(state.allStatuses, status) + const newStatus = state.allStatusesObject[status.id] newStatus.repeated = value }, setDeleted (state, { status }) { - const newStatus = find(state.allStatuses, status) + const newStatus = state.allStatusesObject[status.id] newStatus.deleted = true }, setLoading (state, { timeline, value }) { state.timelines[timeline].loading = value }, setNsfw (state, { id, nsfw }) { - const newStatus = find(state.allStatuses, { id }) + const newStatus = state.allStatusesObject[id] newStatus.nsfw = nsfw }, + setError (state, { timeline, value }) { + state.timelines[timeline].error = value + }, markNotificationsAsSeen (state, notifications) { each(notifications, (notification) => { notification.seen = true @@ -293,6 +314,9 @@ const statuses = { addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false }) { commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser }) }, + setError ({ rootState, commit }, { timeline, value }) { + commit('setError', { timeline, value }) + }, deleteStatus ({ rootState, commit }, status) { commit('setDeleted', { status }) apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials }) diff --git a/src/modules/users.js b/src/modules/users.js index 31731880..22e0133c 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -1,11 +1,11 @@ import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' -import { compact, map, each, find, merge } from 'lodash' +import { compact, map, each, merge } from 'lodash' import { set } from 'vue' // TODO: Unify with mergeOrAdd in statuses.js -export const mergeOrAdd = (arr, item) => { +export const mergeOrAdd = (arr, obj, item) => { if (!item) { return false } - const oldItem = find(arr, {id: item.id}) + const oldItem = obj[item.id] if (oldItem) { // We already have this, so only merge the new info. merge(oldItem, item) @@ -13,13 +13,14 @@ export const mergeOrAdd = (arr, item) => { } else { // This is a new item, prepare it arr.push(item) + obj[item.id] = item return {item, new: true} } } export const mutations = { setMuted (state, { user: {id}, muted }) { - const user = find(state.users, {id}) + const user = state.usersObject[id] set(user, 'muted', muted) }, setCurrentUser (state, user) { @@ -32,17 +33,18 @@ export const mutations = { state.loggingIn = false }, addNewUsers (state, users) { - each(users, (user) => mergeOrAdd(state.users, user)) + each(users, (user) => mergeOrAdd(state.users, state.usersObject, user)) }, setUserForStatus (state, status) { - status.user = find(state.users, status.user) + status.user = state.usersObject[status.user.id] } } export const defaultState = { currentUser: false, loggingIn: false, - users: [] + users: [], + usersObject: {} } const users = { @@ -65,40 +67,52 @@ const users = { }) }, loginUser (store, userCredentials) { - const commit = store.commit - commit('beginLogin') - store.rootState.api.backendInteractor.verifyCredentials(userCredentials) - .then((response) => { - if (response.ok) { - response.json() - .then((user) => { - user.credentials = userCredentials - commit('setCurrentUser', user) - commit('addNewUsers', [user]) + return new Promise((resolve, reject) => { + const commit = store.commit + commit('beginLogin') + store.rootState.api.backendInteractor.verifyCredentials(userCredentials) + .then((response) => { + if (response.ok) { + response.json() + .then((user) => { + user.credentials = userCredentials + commit('setCurrentUser', user) + commit('addNewUsers', [user]) - // Set our new backend interactor - commit('setBackendInteractor', backendInteractorService(userCredentials)) + // Set our new backend interactor + commit('setBackendInteractor', backendInteractorService(userCredentials)) - // Start getting fresh tweets. - store.dispatch('startFetching', 'friends') + // Start getting fresh tweets. + store.dispatch('startFetching', 'friends') - // Get user mutes and follower info - store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => { - each(mutedUsers, (user) => { user.muted = true }) - store.commit('addNewUsers', mutedUsers) - }) + // Get user mutes and follower info + store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => { + each(mutedUsers, (user) => { user.muted = true }) + store.commit('addNewUsers', mutedUsers) + }) - // Fetch our friends - store.rootState.api.backendInteractor.fetchFriends() - .then((friends) => commit('addNewUsers', friends)) - }) - } - commit('endLogin') - }) - .catch((error) => { - console.log(error) - commit('endLogin') - }) + // Fetch our friends + store.rootState.api.backendInteractor.fetchFriends() + .then((friends) => commit('addNewUsers', friends)) + }) + } else { + // Authentication failed + commit('endLogin') + if (response.status === 401) { + reject('Wrong username or password') + } else { + reject('An error occurred, please try again') + } + } + commit('endLogin') + resolve() + }) + .catch((error) => { + console.log(error) + commit('endLogin') + reject('Failed to connect to server, try again') + }) + }) } } } diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 37bbcd82..e684a170 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -5,6 +5,8 @@ import apiService from '../api/api.service.js' const update = ({store, statuses, timeline, showImmediately}) => { const ccTimeline = camelCase(timeline) + setError({store, timeline, value: false}) + store.dispatch('addNewStatuses', { timeline: ccTimeline, statuses, @@ -12,6 +14,15 @@ const update = ({store, statuses, timeline, showImmediately}) => { }) } +const setError = ({store, timeline, value}) => { + const ccTimeline = camelCase(timeline) + + store.dispatch('setError', { + timeline: ccTimeline, + value + }) +} + const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false}) => { const args = { timeline, credentials } const rootState = store.rootState || store.state @@ -24,7 +35,8 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false } return apiService.fetchTimeline(args) - .then((statuses) => update({store, statuses, timeline, showImmediately})) + .then((statuses) => update({store, statuses, timeline, showImmediately}), + () => setError({store, timeline, value: true})) } const startFetching = ({ timeline = 'friends', credentials, store }) => { |
