diff options
| author | Roger Braun <roger@rogerbraun.net> | 2017-02-16 17:49:11 +0100 |
|---|---|---|
| committer | Roger Braun <roger@rogerbraun.net> | 2017-02-16 17:49:11 +0100 |
| commit | f25ae61c5daf7e925fb38d1636a61b70a4dd2d1c (patch) | |
| tree | 883faa9a3bf7999c1e290d140951e35c13ae45a2 /src | |
| parent | 319af91a4762c7edab567048f1f44b5a26005d5b (diff) | |
| parent | ce5b3d4c924d6e94b6fbde3c50fdb209e4ec1fab (diff) | |
Merge branch 'develop' into feature/hash-routed
Diffstat (limited to 'src')
| -rw-r--r-- | src/App.js | 7 | ||||
| -rw-r--r-- | src/App.scss | 4 | ||||
| -rw-r--r-- | src/App.vue | 4 | ||||
| -rw-r--r-- | src/components/public_and_external_timeline/public_and_external_timeline.js | 6 | ||||
| -rw-r--r-- | src/components/public_timeline/public_timeline.js | 7 | ||||
| -rw-r--r-- | src/components/status/status.js | 17 | ||||
| -rw-r--r-- | src/components/status/status.vue | 133 | ||||
| -rw-r--r-- | src/components/style_switcher/style_switcher.js | 5 | ||||
| -rw-r--r-- | src/components/user_card_content/user_card_content.vue | 8 | ||||
| -rw-r--r-- | src/main.js | 23 | ||||
| -rw-r--r-- | src/modules/api.js | 23 | ||||
| -rw-r--r-- | src/modules/config.js | 30 | ||||
| -rw-r--r-- | src/modules/statuses.js | 6 | ||||
| -rw-r--r-- | src/modules/users.js | 24 | ||||
| -rw-r--r-- | src/services/api/api.service.js | 10 | ||||
| -rw-r--r-- | src/services/backend_interactor_service/backend_interactor_service.js | 13 | ||||
| -rw-r--r-- | src/services/style_setter/style_setter.js | 2 | ||||
| -rw-r--r-- | src/services/timeline_fetcher/timeline_fetcher.service.js | 3 |
18 files changed, 247 insertions, 78 deletions
@@ -16,7 +16,12 @@ export default { }), computed: { currentUser () { return this.$store.state.users.currentUser }, - style () { return { 'background-image': `url(${this.currentUser.background_image})` } } + background () { + return this.currentUser.background_image || this.$store.state.config.background + }, + logoStyle () { return { 'background-image': `url(${this.$store.state.config.logo})` } }, + style () { return { 'background-image': `url(${this.background})` } }, + sitename () { return this.$store.state.config.name } }, methods: { activatePanel (panelName) { diff --git a/src/App.scss b/src/App.scss index c820779a..d39fc749 100644 --- a/src/App.scss +++ b/src/App.scss @@ -63,6 +63,10 @@ nav { align-items: center; flex-basis: 920px; margin: auto; + height: 50px; + background-repeat: no-repeat; + background-position: center; + background-size: contain; } } diff --git a/src/App.vue b/src/App.vue index 5d5463fb..a22307a6 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,9 +1,9 @@ <template> <div id="app" v-bind:style="style" class="base02-background"> <nav class='container base01-background base04'> - <div class='inner-nav'> + <div class='inner-nav' :style="logoStyle"> <div class='item'> - <a route-to='friends-timeline' href="#">Pleroma FE</a> + <a route-to='friends-timeline' href="#">{{sitename}}</a> </div> <style-switcher></style-switcher> </div> diff --git a/src/components/public_and_external_timeline/public_and_external_timeline.js b/src/components/public_and_external_timeline/public_and_external_timeline.js index 138118ad..0db6efae 100644 --- a/src/components/public_and_external_timeline/public_and_external_timeline.js +++ b/src/components/public_and_external_timeline/public_and_external_timeline.js @@ -5,6 +5,12 @@ const PublicAndExternalTimeline = { }, computed: { timeline () { return this.$store.state.statuses.timelines.publicAndExternal } + }, + created () { + this.$store.dispatch('startFetching', 'publicAndExternal') + }, + destroyed () { + this.$store.dispatch('stopFetching', 'publicAndExternal') } } diff --git a/src/components/public_timeline/public_timeline.js b/src/components/public_timeline/public_timeline.js index cac422ec..9b866be8 100644 --- a/src/components/public_timeline/public_timeline.js +++ b/src/components/public_timeline/public_timeline.js @@ -5,7 +5,14 @@ const PublicTimeline = { }, computed: { timeline () { return this.$store.state.statuses.timelines.public } + }, + created () { + this.$store.dispatch('startFetching', 'public') + }, + destroyed () { + this.$store.dispatch('stopFetching', 'public') } + } export default PublicTimeline diff --git a/src/components/status/status.js b/src/components/status/status.js index 40589ea5..030e22b5 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -3,6 +3,7 @@ import FavoriteButton from '../favorite_button/favorite_button.vue' import RetweetButton from '../retweet_button/retweet_button.vue' import DeleteButton from '../delete_button/delete_button.vue' import PostStatusForm from '../post_status_form/post_status_form.vue' +import UserCardContent from '../user_card_content/user_card_content.vue' const Status = { props: [ @@ -11,7 +12,9 @@ const Status = { ], data: () => ({ replying: false, - expanded: false + expanded: false, + unmuted: false, + userExpanded: false }), computed: { retweet () { return !!this.statusoid.retweeted_status }, @@ -25,14 +28,16 @@ const Status = { }, loggedIn () { return !!this.$store.state.users.currentUser - } + }, + muted () { return !this.unmuted && this.status.user.muted } }, components: { Attachment, FavoriteButton, RetweetButton, DeleteButton, - PostStatusForm + PostStatusForm, + UserCardContent }, methods: { toggleReplying () { @@ -40,6 +45,12 @@ const Status = { }, toggleExpanded () { this.$emit('toggleExpanded') + }, + toggleMute () { + this.unmuted = !this.unmuted + }, + toggleUserExpanded () { + this.userExpanded = !this.userExpanded } } } diff --git a/src/components/status/status.vue b/src/components/status/status.vue index f113fb7e..0c004936 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -1,70 +1,84 @@ <template> <div class="status-el base00-background" v-if="!status.deleted"> - <div v-if="retweet" class="media container retweet-info"> - <div class="media-left"> - <i class='fa icon-retweet retweeted'></i> + <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> + <a href="#" class="unmute" @click.prevent="toggleMute"><i class="icon-eye-off"></i></a> </div> - <div class="media-body"> - Retweeted by {{retweeter}} - </div> - </div> - <div class="media status container"> - <div class="media-left"> - <a :href="status.user.statusnet_profile_url"> - <img class='avatar' :src="status.user.profile_image_url_original"> - </a> + </template> + <template v-if="!muted"> + <div v-if="retweet" class="media container retweet-info"> + <div class="media-left"> + <i class='fa icon-retweet retweeted'></i> + </div> + <div class="media-body"> + Retweeted by {{retweeter}} + </div> </div> - <div class="media-body"> - <div class="user-content"> - <h4 class="media-heading"> - {{status.user.name}} - <small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small> - <small v-if="status.in_reply_to_screen_name"> > - <router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }"> - {{status.in_reply_to_screen_name}} - </router-link> - </small> - - - <small> - <router-link :to="{ name: 'conversation', params: { id: status.id } }"> - <timeago :since="status.created_at" :auto-update="60"></timeago> - </router-link> - </small> - <template v-if="expandable"> + <div class="media status container"> + <div class="media-left"> + <a :href="status.user.statusnet_profile_url"> + <img @click.prevent="toggleUserExpanded" class='avatar' :src="status.user.profile_image_url_original"> + </a> + </div> + <div class="media-body"> + <div class="base05 base05=border usercard" v-if="userExpanded"> + <user-card-content :user="status.user"></user-card-content> + </div> + <div class="user-content"> + <h4 class="media-heading"> + {{status.user.name}} + <small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small> + <small v-if="status.in_reply_to_screen_name"> > + <router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }"> + {{status.in_reply_to_screen_name}} + </router-link> + </small> - <small> - <a href="#" @click.prevent="toggleExpanded" >Expand</a> + <router-link :to="{ name: 'conversation', params: { id: status.id } }"> + <timeago :since="status.created_at" :auto-update="60"></timeago> + </router-link> </small> - </template> - <small v-if="!status.is_local" class="source_url"> - <a :href="status.external_url" target="_blank" >Source</a> - </small> - </h4> + <template v-if="expandable"> + - + <small> + <a href="#" @click.prevent="toggleExpanded" ><i class="icon-plus-squared"></i></a> + </small> + <small v-if="status.user.muted"> + <a href="#" @click.prevent="toggleMute" ><i class="icon-eye-off"></i></a> + </small> + </template> + <small v-if="!status.is_local" class="source_url"> + <a :href="status.external_url" target="_blank" ><i class="icon-binoculars"></i></a> + </small> + </h4> - <div class="status-content" v-html="status.statusnet_html"></div> + <div class="status-content" v-html="status.statusnet_html"></div> - <div v-if='status.attachments' class='attachments'> - <attachment :status-id="status.id" :nsfw="status.nsfw" :attachment="attachment" v-for="attachment in status.attachments"> - </attachment> + <div v-if='status.attachments' class='attachments'> + <attachment :status-id="status.id" :nsfw="status.nsfw" :attachment="attachment" v-for="attachment in status.attachments"> + </attachment> + </div> </div> - </div> - <div v-if="loggedIn"> - <div class='status-actions'> - <div> - <a href="#" v-on:click.prevent="toggleReplying"> - <i class='fa icon-reply'></i> - </a> + <div v-if="loggedIn"> + <div class='status-actions'> + <div> + <a href="#" v-on:click.prevent="toggleReplying"> + <i class='fa icon-reply'></i> + </a> + </div> + <retweet-button :status=status></retweet-button> + <favorite-button :status=status></favorite-button> + <delete-button :status=status></delete-button> </div> - <retweet-button :status=status></retweet-button> - <favorite-button :status=status></favorite-button> - <delete-button :status=status></delete-button> - </div> - <post-status-form v-if="replying" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" v-on:posted="toggleReplying"></post-status-form> + <post-status-form v-if="replying" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" v-on:posted="toggleReplying"></post-status-form> + </div> </div> </div> - </div> + </template> </div> </template> @@ -128,4 +142,19 @@ padding-right: 1em; border-bottom: 1px solid; } + .muted button { + margin-left: auto; + } + + a.unmute { + display: block; + margin-left: auto; + } + + .usercard { + border-style: solid; + border-width: 1px; + border-radius: 1em; + margin-bottom: 1em; + } </style> diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 8a8cc2a0..954e2ff0 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -1,5 +1,3 @@ -import StyleSetter from '../../services/style_setter/style_setter.js' - export default { data: () => ({ availableStyles: [], @@ -13,8 +11,7 @@ export default { }, watch: { selected () { - const fullPath = `/static/css/${this.selected}` - StyleSetter.setStyle(fullPath) + this.$store.dispatch('setOption', { name: 'theme', value: this.selected }) } } } diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue index 9a21f404..2c32406b 100644 --- a/src/components/user_card_content/user_card_content.vue +++ b/src/components/user_card_content/user_card_content.vue @@ -3,6 +3,10 @@ <div class="base00-background panel-heading text-center" v-bind:style="style"> <div class='user-info'> <img :src="user.profile_image_url"> + <div v-if='user.muted' class='muteinfo'>Muted</div> + <div class='muteinfo' v-if='isOtherUser'> + <button @click="toggleMute">Mute/Unmute</button> + </div> <span class="glyphicon glyphicon-user"></span> <div class='user-name'>{{user.name}}</div> <div class='user-screen-name'>@{{user.screen_name}}</div> @@ -70,6 +74,10 @@ const store = this.$store store.state.api.backendInteractor.unfollowUser(this.user.id) .then((unfollowedUser) => store.commit('addNewUsers', [unfollowedUser])) + }, + toggleMute () { + const store = this.$store + store.commit('setMuted', {user: this.user, muted: !this.user.muted}) } } } diff --git a/src/main.js b/src/main.js index 830aa02c..17aeb863 100644 --- a/src/main.js +++ b/src/main.js @@ -12,10 +12,11 @@ import UserProfile from './components/user_profile/user_profile.vue' import statusesModule from './modules/statuses.js' import usersModule from './modules/users.js' import apiModule from './modules/api.js' +import configModule from './modules/config.js' import VueTimeago from 'vue-timeago' -import StyleSetter from './services/style_setter/style_setter.js' +import createPersistedState from 'vuex-persistedstate' Vue.use(Vuex) Vue.use(VueRouter) @@ -26,12 +27,19 @@ Vue.use(VueTimeago, { } }) +const persistedStateOptions = { + paths: ['users.users'] +} + const store = new Vuex.Store({ modules: { statuses: statusesModule, users: usersModule, - api: apiModule - } + api: apiModule, + config: configModule + }, + plugins: [createPersistedState(persistedStateOptions)], + strict: process.env.NODE_ENV !== 'production' }) const routes = [ @@ -60,4 +68,11 @@ new Vue({ components: { App } }) -StyleSetter.setStyle('/static/css/base16-solarized-light.css') +window.fetch('/static/config.json') + .then((res) => res.json()) + .then(({name, theme, background, logo}) => { + store.dispatch('setOption', { name: 'name', value: name }) + store.dispatch('setOption', { name: 'theme', value: theme }) + store.dispatch('setOption', { name: 'background', value: background }) + store.dispatch('setOption', { name: 'logo', value: logo }) + }) diff --git a/src/modules/api.js b/src/modules/api.js index 4000dc60..a32adfde 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -2,11 +2,32 @@ import backendInteractorService from '../services/backend_interactor_service/bac const api = { state: { - backendInteractor: backendInteractorService() + backendInteractor: backendInteractorService(), + fetchers: {} }, mutations: { setBackendInteractor (state, backendInteractor) { state.backendInteractor = backendInteractor + }, + addFetcher (state, {timeline, fetcher}) { + state.fetchers[timeline] = fetcher + }, + removeFetcher (state, {timeline}) { + delete state.fetchers[timeline] + } + }, + actions: { + startFetching (store, timeline) { + // Don't start fetching if we already are. + if (!store.state.fetchers[timeline]) { + const fetcher = store.state.backendInteractor.startFetching({timeline, store}) + store.commit('addFetcher', {timeline, fetcher}) + } + }, + stopFetching (store, timeline) { + const fetcher = store.state.fetchers[timeline] + window.clearInterval(fetcher) + store.commit('removeFetcher', {timeline}) } } } diff --git a/src/modules/config.js b/src/modules/config.js new file mode 100644 index 00000000..4365d554 --- /dev/null +++ b/src/modules/config.js @@ -0,0 +1,30 @@ +import { set } from 'vue' +import StyleSetter from '../services/style_setter/style_setter.js' + +const defaultState = { + name: 'Pleroma FE' +} + +const config = { + state: defaultState, + mutations: { + setOption (state, { name, value }) { + set(state, name, value) + } + }, + actions: { + setOption ({ commit }, { name, value }) { + commit('setOption', {name, value}) + switch (name) { + case 'name': + document.title = value + break + case 'theme': + const fullPath = `/static/css/${value}` + StyleSetter.setStyle(fullPath) + } + } + } +} + +export default config diff --git a/src/modules/statuses.js b/src/modules/statuses.js index b1aa404a..871172b5 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -153,16 +153,18 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us } } + // Decide if we should treat the status as new for this timeline. + let resultForCurrentTimeline // Some statuses should only be added to the global status repository. if (timeline && addToTimeline) { - mergeOrAdd(timelineObject.statuses, status) + resultForCurrentTimeline = mergeOrAdd(timelineObject.statuses, status) } if (timeline && showImmediately) { // Add it directly to the visibleStatuses, don't change // newStatusCount mergeOrAdd(timelineObject.visibleStatuses, status) - } else if (timeline && addToTimeline && result.new) { + } else if (timeline && addToTimeline && resultForCurrentTimeline.new) { // Just change newStatuscount timelineObject.newStatusCount += 1 } diff --git a/src/modules/users.js b/src/modules/users.js index 8ba365f3..ae90abbd 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -1,6 +1,6 @@ -import timelineFetcher from '../services/timeline_fetcher/timeline_fetcher.service.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import { compact, map, each, find, merge } from 'lodash' +import { set } from 'vue' // TODO: Unify with mergeOrAdd in statuses.js export const mergeOrAdd = (arr, item) => { @@ -18,6 +18,10 @@ export const mergeOrAdd = (arr, item) => { } export const mutations = { + setMuted (state, { user: {id}, muted }) { + const user = find(state.users, {id}) + set(user, 'muted', muted) + }, setCurrentUser (state, user) { state.currentUser = user }, @@ -29,6 +33,9 @@ export const mutations = { }, addNewUsers (state, users) { each(users, (user) => mergeOrAdd(state.users, user)) + }, + setUserForStatus (state, status) { + status.user = find(state.users, status.user) } } @@ -47,6 +54,15 @@ const users = { const retweetedUsers = compact(map(statuses, 'retweeted_status.user')) store.commit('addNewUsers', users) store.commit('addNewUsers', retweetedUsers) + + // Reconnect users to statuses + each(statuses, (status) => { + store.commit('setUserForStatus', status) + }) + // Reconnect users to retweets + each(compact(map(statuses, 'retweeted_status')), (status) => { + store.commit('setUserForStatus', status) + }) }, loginUser (store, userCredentials) { const commit = store.commit @@ -60,12 +76,12 @@ const users = { commit('setCurrentUser', user) commit('addNewUsers', [user]) - // Start getting fresh tweets. - timelineFetcher.startFetching({store, credentials: userCredentials}) - // Set our new backend interactor commit('setBackendInteractor', backendInteractorService(userCredentials)) + // Start getting fresh tweets. + store.dispatch('startFetching', 'friends') + // Fetch our friends store.rootState.api.backendInteractor.fetchFriends() .then((friends) => commit('addNewUsers', friends)) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 87a7e2e5..de89f503 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1,6 +1,7 @@ /* eslint-env browser */ const LOGIN_URL = '/api/account/verify_credentials.json' const FRIENDS_TIMELINE_URL = '/api/statuses/friends_timeline.json' +const ALL_FOLLOWING_URL = '/api/qvitter/allfollowing' const PUBLIC_TIMELINE_URL = '/api/statuses/public_timeline.json' const PUBLIC_AND_EXTERNAL_TIMELINE_URL = '/api/statuses/public_and_external_timeline.json' const FAVORITE_URL = '/api/favorites/create' @@ -54,6 +55,12 @@ const fetchFriends = ({credentials}) => { .then((data) => data.json()) } +const fetchAllFollowing = ({username, credentials}) => { + const url = `${ALL_FOLLOWING_URL}/${username}.json` + return fetch(url, { headers: authHeaders(credentials) }) + .then((data) => data.json().users) +} + const fetchMentions = ({username, sinceId = 0, credentials}) => { let url = `${MENTIONS_URL}?since_id=${sinceId}&screen_name=${username}` return fetch(url, { headers: authHeaders(credentials) }) @@ -169,7 +176,8 @@ const apiService = { retweet, postStatus, deleteStatus, - uploadMedia + uploadMedia, + fetchAllFollowing } export default apiService diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 746776bf..d335bfb7 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -1,4 +1,5 @@ import apiService from '../api/api.service.js' +import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js' const backendInteractorService = (credentials) => { const fetchStatus = ({id}) => { @@ -17,6 +18,10 @@ const backendInteractorService = (credentials) => { return apiService.fetchFriends({credentials}) } + const fetchAllFollowing = ({username}) => { + return apiService.fetchAllFollowing({username, credentials}) + } + const followUser = (id) => { return apiService.followUser({credentials, id}) } @@ -25,6 +30,10 @@ const backendInteractorService = (credentials) => { return apiService.unfollowUser({credentials, id}) } + const startFetching = ({timeline, store}) => { + return timelineFetcherService.startFetching({timeline, store, credentials}) + } + const backendInteractorServiceInstance = { fetchStatus, fetchConversation, @@ -32,7 +41,9 @@ const backendInteractorService = (credentials) => { fetchFriends, followUser, unfollowUser, - verifyCredentials: apiService.verifyCredentials + fetchAllFollowing, + verifyCredentials: apiService.verifyCredentials, + startFetching } return backendInteractorServiceInstance diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 79b68b38..0a5be77d 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -34,7 +34,7 @@ const setStyle = (href) => { styleSheet.insertRule(`a { color: ${base08Color}`, 'index-max') styleSheet.insertRule(`body { color: ${base05Color}`, 'index-max') - styleSheet.insertRule(`.base05-border { color: ${base05Color}`, 'index-max') + styleSheet.insertRule(`.base05-border { border-color: ${base05Color}`, 'index-max') body.style.display = 'initial' } cssEl.addEventListener('load', setDynamic) diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 0d4ffcad..37bbcd82 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -30,8 +30,7 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false const startFetching = ({ timeline = 'friends', credentials, store }) => { fetchAndUpdate({timeline, credentials, store, showImmediately: true}) const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store }) - - setInterval(boundFetchAndUpdate, 10000) + return setInterval(boundFetchAndUpdate, 10000) } const timelineFetcher = { fetchAndUpdate, |
