diff options
| author | Roger Braun <roger@rogerbraun.net> | 2017-02-20 18:58:18 +0100 |
|---|---|---|
| committer | Roger Braun <roger@rogerbraun.net> | 2017-02-20 18:58:18 +0100 |
| commit | 5d8b2eb8b57efa36f9bde45fbbffc2e19f675eb4 (patch) | |
| tree | f7883fdbe27a2cfd81ff72437450976cc243d2f2 /src | |
| parent | 9d0d1f7de1e37cb40d7ecd34fd93744f55142e2b (diff) | |
| parent | 73afa8e075cd8e97b09526e2393b0aafc9c8e4e5 (diff) | |
Merge branch 'develop' into feature/hash-routed
Diffstat (limited to 'src')
22 files changed, 302 insertions, 60 deletions
@@ -24,6 +24,9 @@ export default { methods: { activatePanel (panelName) { this.mobileActivePanel = panelName + }, + scrollToTop () { + window.scrollTo(0, 0) } } } diff --git a/src/App.scss b/src/App.scss index 5c14c17b..431aeb32 100644 --- a/src/App.scss +++ b/src/App.scss @@ -29,6 +29,15 @@ a { text-decoration: none; } +button{ + border: none; + border-radius: 5px; + + &:hover { + background-color: white; + } +} + .container { display: flex; flex-wrap: wrap; @@ -63,7 +72,7 @@ nav { padding-right: 20px; display: flex; align-items: center; - flex-basis: 920px; + flex-basis: 970px; margin: auto; height: 50px; background-repeat: no-repeat; @@ -99,10 +108,10 @@ main-router { .panel-heading { border-radius: 0.5em 0.5em 0 0; background-size: cover; - padding-top: 0.3em; - padding-bottom: 0.3em; + padding: 0.6em 0; text-align: center; font-size: 1.3em; + line-height: 24px; } .panel-footer { @@ -110,6 +119,7 @@ main-router { } .panel-body > p { + line-height: 18px; padding: 1em; margin: 0; } @@ -117,7 +127,7 @@ main-router { #content { margin: auto; - max-width: 920px; + max-width: 980px; border-radius: 1em; padding-bottom: 1em; background-color: rgba(0,0,0,0.1); @@ -125,7 +135,7 @@ main-router { .media-body { flex: 1; - padding-left: 0.3em; + padding-left: 0.5em; } .container > * { @@ -133,28 +143,37 @@ main-router { } .user-info { + color: white; padding: 1em; img { - border: 3px solid; + border: 2px solid; border-radius: 0.5em } - + text-shadow: 0 1px 1px rgba(0, 0, 0, 0.5); + .user-name{ + margin-top: 0.2em; + } .user-screen-name { + margin-top: 0.3em; font-weight: lighter; + padding-right: 0.1em; } } .user-counts { display: flex; - padding: 1em 1em 0em 1em; + line-height:16px; + padding: 1em 1.5em 0em 1em; + text-align: center; } .user-count { flex: 1; h5 { - font-weight: lighter; - margin: 0; + font-size:1em; + font-weight: bolder; + margin: 0 0 0.25em; } } @@ -196,7 +215,7 @@ status-text-container { } .retweet-info { - padding: 0.3em; + padding: 0.7em 0 0 0.6em; .media-left { display: flex; @@ -214,6 +233,7 @@ status-text-container { small { font-weight: lighter; } + margin-bottom: 0.3em; } } nav { @@ -228,13 +248,13 @@ nav { } .main { - flex: 2; - flex-basis: 500px; + flex: 1; + flex-basis: 65%; } .sidebar { flex: 1; - flex-basis: 300px; + flex-basis: 35%; } .mobile-shown { @@ -261,6 +281,14 @@ nav { .panel-switcher { display: flex; } + + .container { + padding: 0 0 0 0; + } + + .panel { + margin: 0.5em 0 0.5em 0; + } } .item.right { diff --git a/src/App.vue b/src/App.vue index d8e1d3e8..c4b3cb13 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,6 +1,6 @@ <template> <div id="app" v-bind:style="style" class="base02-background"> - <nav class='container base01-background base04'> + <nav class='container base01-background base04' @click="scrollToTop()"> <div class='inner-nav' :style="logoStyle"> <div class='item'> <router-link :to="{ name: 'root'}">{{sitename}}</router-link> diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index c88497a2..57d21b28 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -20,6 +20,11 @@ const Attachment = { } }, methods: { + linkClicked ({target}) { + if (target.tagName === 'A') { + window.open(target.href, '_blank') + } + }, toggleHidden () { this.showHidden = !this.showHidden } diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 311e36b8..738a1e86 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -18,7 +18,7 @@ <span v-if="type === 'unknown'">Don't know how to display this...</span> - <div v-if="type === 'html' && attachment.oembed" class="oembed"> + <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> </div> @@ -39,7 +39,7 @@ .attachment { flex: 1 0 30%; display: flex; - margin: 0.2em; + margin: 0.5em 0.8em 0.6em 0.1em; align-self: flex-start; &.html { @@ -79,6 +79,7 @@ img { width: 100%; } + margin-right: 15px; } .oembed { @@ -91,6 +92,8 @@ img { border: 0px; border-radius: 0; + height: 100%; + object-fit: cover; } } diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 2082f41a..85ed163c 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -39,8 +39,7 @@ .nav-panel li { border-bottom: 1px solid; - padding: 0.5em; - padding-left: 1em; + padding: 0.8em 0.85em; } .nav-panel li:last-child { diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 10f987a8..c8d5e212 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -1,14 +1,40 @@ -import { sortBy, take } from 'lodash' +import { sortBy, take, filter } from 'lodash' const Notifications = { data () { return { - visibleNotificationCount: 20 + visibleNotificationCount: 10 } }, computed: { + notifications () { + return this.$store.state.statuses.notifications + }, + unseenNotifications () { + return filter(this.notifications, ({seen}) => !seen) + }, visibleNotifications () { - return take(sortBy(this.$store.state.statuses.notifications, ({action}) => -action.id), this.visibleNotificationCount) + // Don't know why, but sortBy([seen, -action.id]) doesn't work. + let sortedNotifications = sortBy(this.notifications, ({action}) => -action.id) + sortedNotifications = sortBy(sortedNotifications, 'seen') + return take(sortedNotifications, this.visibleNotificationCount) + }, + unseenCount () { + return this.unseenNotifications.length + } + }, + watch: { + unseenCount (count) { + if (count > 0) { + this.$store.dispatch('setPageTitle', `(${count})`) + } else { + this.$store.dispatch('setPageTitle', '') + } + } + }, + methods: { + markAsSeen () { + this.$store.commit('markNotificationsAsSeen', this.visibleNotifications) } } } diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index 687a4f0f..517afeaa 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -1,13 +1,13 @@ @import '../../_variables.scss'; .notification { - padding: 0.5em; - padding-left: 1em; + padding: 0.4em 0 0 0.7em; display: flex; border-bottom: 1px solid silver; .text { min-width: 0px; word-wrap: break-word; + line-height:18px; .icon-retweet { color: $green; @@ -18,21 +18,22 @@ } h1 { - margin: 0; + margin: 0 0 0.3em; padding: 0; font-size: 1em; + line-height:20px; } - padding-left: 0.5em; + padding: 0.3em 0.8em 0.5em; p { margin: 0; margin-top: 0; - margin-bottom: 0.5em; + margin-bottom: 0.3em; } } .avatar { - padding-top: 3px; + padding-top: 0.3em; width: 32px; height: 32px; border-radius: 50%; diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue index 0846c27b..785cc019 100644 --- a/src/components/notifications/notifications.vue +++ b/src/components/notifications/notifications.vue @@ -1,13 +1,14 @@ <template> <div class="notifications"> <div class="panel panel-default base00-background"> - <div class="panel-heading base01-background base04">Notifications ({{visibleNotifications.length}})</div> + <div class="panel-heading base01-background base04">Notifications ({{unseenCount}}) <button @click.prevent="markAsSeen">Read!</button></div> <div class="panel-body"> - <div v-for="notification in visibleNotifications" class="notification"> + <div v-for="notification in visibleNotifications" class="notification" :class='{"base01-background": notification.seen}'> <a :href="notification.action.user.statusnet_profile_url"> <img class='avatar' :src="notification.action.user.profile_image_url_original"> </a> <div class='text'> + <timeago :since="notification.action.created_at" :auto-update="240"></timeago> <div v-if="notification.type === 'favorite'"> <h1>{{ notification.action.user.name }}<br><i class="fa icon-star"></i> favorited your <router-link :to="{ name: 'conversation', params: { id: notification.status.id } }">status</h1> <p>{{ notification.status.text }}</p> diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index a6aaf511..1bad41c7 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -2,7 +2,7 @@ <div class="post-status-form"> <form @submit.prevent="postStatus(newStatus)"> <div class="form-group" > - <textarea v-model="newStatus.status" placeholder="Just landed in L.A." rows="3" class="form-control"></textarea> + <textarea v-model="newStatus.status" placeholder="Just landed in L.A." rows="3" class="form-control" @keyup.ctrl.enter="postStatus(newStatus)"></textarea> </div> <div class="attachments"> <div class="attachment" v-for="file in newStatus.files"> @@ -57,13 +57,22 @@ form { display: flex; flex-direction: column; - padding: 0.5em; + padding: 0.6em; } .form-group { display: flex; flex-direction: column; + padding: 0.3em 0.5em 0.6em; + line-height:24px; + } + + form textarea { + border: none; + border-radius: 2px; + line-height:16px; padding: 0.5em; + resize: vertical; } .btn { diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index 8bb0ffb1..57aafac8 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -17,6 +17,6 @@ <style> .setting-item { - margin: 1em + margin: 1em 1em 1.4em; } </style> diff --git a/src/components/status/status.js b/src/components/status/status.js index 030e22b5..46add8aa 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -40,6 +40,14 @@ const Status = { UserCardContent }, methods: { + linkClicked ({target}) { + if (target.tagName === 'SPAN') { + target = target.parentNode + } + if (target.tagName === 'A') { + window.open(target.href, '_blank') + } + }, toggleReplying () { this.replying = !this.replying }, diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 0c004936..162ab140 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -54,7 +54,7 @@ </small> </h4> - <div class="status-content" v-html="status.statusnet_html"></div> + <div @click.prevent="linkClicked" 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"> @@ -94,6 +94,7 @@ .user-content { min-height: 52px; + padding-top: 1px; } .source_url { @@ -110,8 +111,7 @@ } .status-content { - margin-top: 3px; - margin-bottom: 3px; + margin: 3px 15px 4px 0; } p { @@ -138,8 +138,7 @@ } .status { - padding: 0.5em; - padding-right: 1em; + padding: 0.65em 0.7em 0.8em 0.8em; border-bottom: 1px solid; } .muted button { diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue index 2c32406b..59cee734 100644 --- a/src/components/user_card_content/user_card_content.vue +++ b/src/components/user_card_content/user_card_content.vue @@ -3,30 +3,34 @@ <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> - <div v-if="isOtherUser" class="following-info"> - <div v-if="user.follows_you" class="following"> + <div v-if="isOtherUser" class="user-interactions"> + <div v-if="user.follows_you" class="following base06"> Follows you! </div> - <div class="followed"> + <div class="follow"> <span v-if="user.following"> - Following them! - <button @click="unfollowUser"> - Unfollow! + <!--Following them!--> + <button @click="unfollowUser" class="base06 base01-background base06-border"> + Unfollow </button> </span> - <span v-if="!user.following" > - <button @click="followUser"> - Follow! + <span v-if="!user.following"> + <button @click="followUser" class="base01 base04-background base01-border"> + Follow </button> </span> </div> + <div class='mute' v-if='isOtherUser'> + <span v-if='user.muted'> + <button @click="toggleMute" class="base04 base01-background base06-border">Unmute</button> + </span> + <span v-if='!user.muted'> + <button @click="toggleMute" class="base01 base04-background base01-border">Mute</button> + </span> + </div> </div> </div> </div> @@ -78,6 +82,7 @@ toggleMute () { const store = this.$store store.commit('setMuted', {user: this.user, muted: !this.user.muted}) + store.state.api.backendInteractor.setUserMute(this.user) } } } diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index fe9fd3a9..b0d6db85 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -13,12 +13,39 @@ } .user-info { - .following-info { + .user-interactions { display: flex; + flex-flow: row wrap; + justify-content: center; div { flex: 1; } + margin-top: 0.5em; + margin-bottom: -1.2em; + + .following { + font-size: 14px; + flex: 0 0 100%; + margin-bottom: 0.5em; + } + + .mute { + max-width: 200px; + } + + .follow { + max-width: 200px; + } + + button { + width: 80%; + height: 100%; + border: 1px solid; + } + } + .user-screen-name { + margin-top: 0.4em; } } </style> diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js new file mode 100644 index 00000000..de1e5383 --- /dev/null +++ b/src/lib/persisted_state.js @@ -0,0 +1,76 @@ +import merge from 'lodash.merge' +import objectPath from 'object-path' +import { throttle } from 'lodash' +import { inflate, deflate } from 'pako' + +const defaultReducer = (state, paths) => ( + paths.length === 0 ? state : paths.reduce((substate, path) => { + objectPath.set(substate, path, objectPath.get(state, path)) + return substate + }, {}) +) + +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() +})() + +const defaultSetState = (key, state, storage) => { + return storage.setItem(key, deflate(JSON.stringify(state), { to: 'string' })) +} + +export default function createPersistedState ({ + key = 'vuex', + paths = [], + getState = (key, storage) => { + let value = storage.getItem(key) + try { + value = inflate(value, { to: 'string' }) + } catch (e) { + console.log("Couldn't inflate value... Maybe upgrading") + } + return value && value !== 'undefined' ? JSON.parse(value) : undefined + }, + setState = throttle(defaultSetState, 5000), + reducer = defaultReducer, + storage = defaultStorage, + subscriber = store => handler => store.subscribe(handler) +} = {}) { + return store => { + const savedState = getState(key, storage) + if (typeof savedState === 'object') { + store.replaceState( + merge({}, store.state, savedState) + ) + } + + subscriber(store)((mutation, state) => { + try { + setState(key, reducer(state, paths), storage) + } catch (e) { + console.log("Couldn't persist state:") + console.log(e) + } + }) + } +} diff --git a/src/main.js b/src/main.js index c11217d4..3d365095 100644 --- a/src/main.js +++ b/src/main.js @@ -17,7 +17,7 @@ import configModule from './modules/config.js' import VueTimeago from 'vue-timeago' -import createPersistedState from 'vuex-persistedstate' +import createPersistedState from './lib/persisted_state.js' Vue.use(Vuex) Vue.use(VueRouter) @@ -29,7 +29,7 @@ Vue.use(VueTimeago, { }) const persistedStateOptions = { - paths: ['users.users'] + paths: ['users.users', 'statuses.notifications'] } const store = new Vuex.Store({ diff --git a/src/modules/config.js b/src/modules/config.js index 4365d554..a1276519 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -13,11 +13,14 @@ const config = { } }, actions: { - setOption ({ commit }, { name, value }) { + setPageTitle ({state}, option = '') { + document.title = `${option} ${state.name}` + }, + setOption ({ commit, dispatch }, { name, value }) { commit('setOption', {name, value}) switch (name) { case 'name': - document.title = value + dispatch('setPageTitle') break case 'theme': const fullPath = `/static/css/${value}` diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 871172b5..491d0024 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -173,7 +173,10 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us } const addNotification = ({type, status, action}) => { - state.notifications.push({type, status, action}) + // Only add a new notification if we don't have one for the same action + if (!find(state.notifications, (oldNotification) => oldNotification.action.id === action.id)) { + state.notifications.push({type, status, action, seen: false}) + } } const favoriteStatus = (favorite) => { @@ -276,6 +279,11 @@ export const mutations = { setNsfw (state, { id, nsfw }) { const newStatus = find(state.allStatuses, { id }) newStatus.nsfw = nsfw + }, + markNotificationsAsSeen (state, notifications) { + each(notifications, (notification) => { + notification.seen = true + }) } } diff --git a/src/modules/users.js b/src/modules/users.js index ae90abbd..31731880 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -82,6 +82,12 @@ const users = { // 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) + }) + // 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 de89f503..f172f769 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -16,6 +16,7 @@ const MENTIONS_URL = '/api/statuses/mentions.json' const FRIENDS_URL = '/api/statuses/friends.json' const FOLLOWING_URL = '/api/friendships/create.json' const UNFOLLOWING_URL = '/api/friendships/destroy.json' +const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json' // const USER_URL = '/api/users/show.json' const oldfetch = window.fetch @@ -58,7 +59,7 @@ const fetchFriends = ({credentials}) => { const fetchAllFollowing = ({username, credentials}) => { const url = `${ALL_FOLLOWING_URL}/${username}.json` return fetch(url, { headers: authHeaders(credentials) }) - .then((data) => data.json().users) + .then((data) => data.json()) } const fetchMentions = ({username, sinceId = 0, credentials}) => { @@ -79,6 +80,22 @@ const fetchStatus = ({id, credentials}) => { .then((data) => data.json()) } +const setUserMute = ({id, credentials, muted = true}) => { + const form = new FormData() + + const muteInteger = muted ? 1 : 0 + + form.append('namespace', 'qvitter') + form.append('data', muteInteger) + form.append('topic', `mute:${id}`) + + return fetch(QVITTER_USER_PREF_URL, { + method: 'POST', + headers: authHeaders(credentials), + body: form + }) +} + const fetchTimeline = ({timeline, credentials, since = false, until = false}) => { const timelineUrls = { public: PUBLIC_TIMELINE_URL, @@ -162,6 +179,14 @@ const uploadMedia = ({formData, credentials}) => { .then((text) => (new DOMParser()).parseFromString(text, 'application/xml')) } +const fetchMutes = ({credentials}) => { + const url = '/api/qvitter/mutes.json' + + return fetch(url, { + headers: authHeaders(credentials) + }).then((data) => data.json()) +} + const apiService = { verifyCredentials, fetchTimeline, @@ -177,7 +202,9 @@ const apiService = { postStatus, deleteStatus, uploadMedia, - fetchAllFollowing + fetchAllFollowing, + setUserMute, + fetchMutes } 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 d335bfb7..d379e602 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -34,6 +34,12 @@ const backendInteractorService = (credentials) => { return timelineFetcherService.startFetching({timeline, store, credentials}) } + const setUserMute = ({id, muted = true}) => { + return apiService.setUserMute({id, muted, credentials}) + } + + const fetchMutes = () => apiService.fetchMutes({credentials}) + const backendInteractorServiceInstance = { fetchStatus, fetchConversation, @@ -43,7 +49,9 @@ const backendInteractorService = (credentials) => { unfollowUser, fetchAllFollowing, verifyCredentials: apiService.verifyCredentials, - startFetching + startFetching, + setUserMute, + fetchMutes } return backendInteractorServiceInstance |
