diff options
| -rw-r--r-- | src/App.scss | 28 | ||||
| -rw-r--r-- | src/App.vue | 14 | ||||
| -rw-r--r-- | src/components/attachment/attachment.js | 8 | ||||
| -rw-r--r-- | src/components/attachment/attachment.vue | 26 | ||||
| -rw-r--r-- | src/components/conversation/conversation.vue | 7 | ||||
| -rw-r--r-- | src/components/post_status_form/post_status_form.vue | 1 | ||||
| -rw-r--r-- | src/components/settings/settings.js | 4 | ||||
| -rw-r--r-- | src/components/settings/settings.vue | 4 | ||||
| -rw-r--r-- | src/components/status/status.vue | 33 | ||||
| -rw-r--r-- | src/components/timeline/timeline.js | 24 | ||||
| -rw-r--r-- | src/components/timeline/timeline.vue | 6 | ||||
| -rw-r--r-- | src/i18n/messages.js | 88 | ||||
| -rw-r--r-- | src/main.js | 1 | ||||
| -rw-r--r-- | src/modules/config.js | 1 | ||||
| -rw-r--r-- | src/modules/statuses.js | 27 | ||||
| -rw-r--r-- | src/modules/users.js | 4 |
16 files changed, 229 insertions, 47 deletions
diff --git a/src/App.scss b/src/App.scss index 0e89a429..5a117f04 100644 --- a/src/App.scss +++ b/src/App.scss @@ -127,6 +127,15 @@ main-router { margin: 0.5em; border-radius: 10px; + box-shadow: 1px 1px 3px rgba(0,0,0,.5); + overflow: hidden; +} + +.panel-body:empty::before { + content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations + display: block; + margin: 1em; + text-align: center; } .panel-heading { @@ -182,7 +191,7 @@ nav { flex-shrink: 1; } -.sidebar { +.sidebar-bounds { flex: 0; flex-basis: 35%; } @@ -210,21 +219,28 @@ nav { } @media all and (min-width: 960px) { - .sidebar { + body { + overflow-y: scroll; + } + .sidebar-bounds { overflow: hidden; max-height: 100vh; - width: 350px; + width: 345px; position: fixed; margin-top: -10px; - .sidebar-container { + .sidebar-scroller { height: 96vh; - width: 362px; + width: 365px; padding-top: 10px; - padding-right: 20px; + padding-right: 50px; overflow-x: hidden; overflow-y: scroll; } + + .sidebar { + width: 345px; + } } .sidebar-flexer { max-height: 96vh; diff --git a/src/App.vue b/src/App.vue index 8e802a02..0d004665 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' @click="scrollToTop()"> + <nav class='container base01-background base04' @click="scrollToTop()" id="nav"> <div class='inner-nav' :style="logoStyle"> <div class='item'> <router-link :to="{ name: 'root'}">{{sitename}}</router-link> @@ -18,11 +18,13 @@ <button @click="activatePanel('timeline')" class="base01-background base04">Timeline</button> </div> <div class="sidebar-flexer" :class="{ 'mobile-hidden': mobileActivePanel != 'sidebar'}"> - <div class="sidebar" :class="{ 'mobile-hidden': mobileActivePanel != 'sidebar' }"> - <div class="sidebar-container"> - <user-panel></user-panel> - <nav-panel></nav-panel> - <notifications v-if="currentUser"></notifications> + <div class="sidebar-bounds"> + <div class="sidebar-scroller"> + <div class="sidebar"> + <user-panel></user-panel> + <nav-panel></nav-panel> + <notifications v-if="currentUser"></notifications> + </div> </div> </div> </div> diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index ccf26b79..de551611 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -23,12 +23,8 @@ const Attachment = { hidden () { return this.nsfw && this.hideNsfwLocal && !this.showHidden }, - autoHeight () { - if (this.type === 'image' && this.nsfw) { - return { - 'min-height': '109px' - } - } + isEmpty () { + return this.type === 'html' && !this.attachment.oembed } }, methods: { diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 98cd97fb..954c46b4 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -1,5 +1,5 @@ <template> - <div class="attachment base03-border" :class="{[type]: true, loading}" :style="autoHeight"> + <div class="attachment base03-border" :class="{[type]: true, loading}" v-show="!isEmpty"> <a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()"> <img :key="nsfwImage" :src="nsfwImage"/> </a> @@ -39,8 +39,19 @@ margin: 0.5em 0.7em 0.6em 0.0em; align-self: flex-start; + border-style: solid; + border-width: 1px; + border-radius: 5px; + overflow: hidden; + + // fixes small gap below video + &.video { + line-height: 0; + } + &.html { - flex-basis: 100%; + flex-basis: 90%; + width: 100%; display: flex; } @@ -53,7 +64,6 @@ margin: 10px; padding: 5px; background: rgba(230,230,230,0.6); - border-radius: 5px; font-weight: bold; z-index: 4; } @@ -61,8 +71,6 @@ video { max-height: 500px; height: 100%; - border: 1px solid; - border-radius: 5px; width: 100%; z-index: 0; } @@ -75,14 +83,9 @@ width: 100%; height: 100%; flex: 1; - border: 1px solid; - border-radius: 5px; } .oembed { - border: 1px solid; - border-radius: 5px; - border-color: inherit; width: 100%; margin-right: 15px; display: flex; @@ -117,9 +120,6 @@ flex: 1; img { - border-style: solid; - border-width: 1px; - border-radius: 5px; object-fit: contain; width: 100%; height: 100%; /* If this isn't here, chrome will stretch the images */ diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index b430d875..3dea4985 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -1,5 +1,5 @@ <template> - <div class="timeline panel panel-default base00-background"> + <div class="timeline panel panel-default"> <div class="panel-heading base01-background base04 base03-border conversation-heading"> {{ $t('timeline.conversation') }} <span v-if="collapsable" style="float:right;"> @@ -27,11 +27,6 @@ <script src="./conversation.js"></script> <style lang="scss"> - .conversation-heading { - border-bottom-style: solid; - border-bottom-width: 1px; - } - .status-preview { position: absolute; max-width: 35em; diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 0a744c10..33614637 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -164,6 +164,7 @@ width: 24px; height: 24px; border-radius: 2px; + object-fit: contain; } span { line-height: 24px; diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index b3bb8290..b88937bb 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -9,6 +9,7 @@ const settings = { hideNsfwLocal: this.$store.state.config.hideNsfw, muteWordsString: this.$store.state.config.muteWords.join('\n'), autoLoadLocal: this.$store.state.config.autoLoad, + streamingLocal: this.$store.state.config.streaming, hoverPreviewLocal: this.$store.state.config.hoverPreview } }, @@ -33,6 +34,9 @@ const settings = { autoLoadLocal (value) { this.$store.dispatch('setOption', { name: 'autoLoad', value }) }, + streamingLocal (value) { + this.$store.dispatch('setOption', { name: 'streaming', value }) + }, hoverPreviewLocal (value) { this.$store.dispatch('setOption', { name: 'hoverPreview', value }) }, diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index 5978e4ed..6a311d55 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -33,6 +33,10 @@ <label for="autoLoad">{{$t('settings.autoload')}}</label> </li> <li> + <input type="checkbox" id="streaming" v-model="streamingLocal"> + <label for="streaming">{{$t('settings.streaming')}}</label> + </li> + <li> <input type="checkbox" id="hoverPreview" v-model="hoverPreviewLocal"> <label for="hoverPreview">{{$t('settings.reply_link_preview')}}</label> </li> diff --git a/src/components/status/status.vue b/src/components/status/status.vue index cc315a90..84397bfa 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -14,7 +14,7 @@ </div> <post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" v-on:posted="toggleReplying" v-if="replying"/> </div> - <div class="status-el base00-background base03-border" v-else-if="!status.deleted" v-bind:class="[{ 'base01-background': isFocused }, { 'status-conversation': inConversation }]" > + <div class="status-el base00-background base03-border status-fadein" v-else-if="!status.deleted" v-bind:class="[{ 'base01-background': isFocused }, { '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> @@ -130,6 +130,11 @@ border-left-width: 0px; line-height: 18px; + .timeline & { + border-bottom-width: 1px; + border-bottom-style: solid; + } + .notify { .avatar { border-width: 3px; @@ -188,6 +193,7 @@ img, video { max-width: 100%; max-height: 400px; + object-fit: contain; } blockquote { @@ -226,6 +232,20 @@ } } + .status-fadein { + animation-duration: 0.5s; + animation-name: fadein; + } + + @keyframes fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + .greentext { color: green; } @@ -279,12 +299,19 @@ .status { padding: 0.4em 0.7em 0.45em 0.7em; - border-bottom: 1px solid; - border-bottom-color: inherit; border-left: 4px rgba(255, 48, 16, 0.65); border-left-style: inherit; } + .status-conversation:last-child { + border-bottom: none; + } + + .timeline .panel.timeline { + border-radius: 10px; + overflow: hidden; + } + .muted { padding: 0.1em 0.4em 0.1em 0.8em; button { diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 6968bc6f..613b8a34 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -11,6 +11,11 @@ const Timeline = { 'userId', 'tag' ], + data () { + return { + paused: false + } + }, computed: { timelineError () { return this.$store.state.statuses.error }, followers () { @@ -21,6 +26,9 @@ const Timeline = { }, viewing () { return this.timeline.viewing + }, + newStatusCount () { + return this.timeline.newStatusCount } }, components: { @@ -56,6 +64,7 @@ const Timeline = { methods: { showNewStatuses () { this.$store.commit('showNewStatuses', { timeline: this.timelineName }) + this.paused = false }, fetchOlderStatuses () { const store = this.$store @@ -90,6 +99,21 @@ const Timeline = { this.fetchOlderStatuses() } } + }, + watch: { + newStatusCount (count) { + if (!this.$store.state.config.streaming) { + return + } + if (count > 0) { + // only 'stream' them when you're scrolled to the top + if (window.pageYOffset < 15 && !this.paused) { + this.showNewStatuses() + } else { + this.paused = true + } + } + } } } diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 01f330c2..18e27c71 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -14,7 +14,7 @@ {{$t('timeline.up_to_date')}} </div> </div> - <div class="panel-body"> + <div class="panel-body base02-background"> <div class="timeline"> <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"> @@ -30,7 +30,7 @@ {{$t('user_card.followers')}} </div> </div> - <div class="panel-body"> + <div class="panel-body base02-background"> <div class="timeline"> <user-card v-for="follower in followers" :user="follower" :showFollows="false"></user-card> </div> @@ -42,7 +42,7 @@ {{$t('user_card.followees')}} </div> </div> - <div class="panel-body"> + <div class="panel-body base02-background"> <div class="timeline"> <user-card v-for="friend in friends" :user="friend" :showFollows="true"></user-card> </div> diff --git a/src/i18n/messages.js b/src/i18n/messages.js index f4147cd8..8671346d 100644 --- a/src/i18n/messages.js +++ b/src/i18n/messages.js @@ -103,6 +103,7 @@ const fi = { hide_attachments_in_convo: 'Piilota liitteet keskusteluissa', nsfw_clickthrough: 'Piilota NSFW liitteet klikkauksen taakse.', autoload: 'Lataa vanhempia viestejä automaattisesti ruudun pohjalla', + streaming: 'Näytä uudet viestit automaattisesti ollessasi ruudun huipulla', reply_link_preview: 'Keskusteluiden vastauslinkkien esikatselu' }, notifications: { @@ -186,6 +187,7 @@ const en = { hide_attachments_in_convo: 'Hide attachments in conversations', nsfw_clickthrough: 'Enable clickthrough NSFW attachment hiding', autoload: 'Enable automatic loading when scrolled to the bottom', + streaming: 'Enable automatic streaming of new posts when scrolled to the top', reply_link_preview: 'Enable reply-link preview on mouse hover' }, notifications: { @@ -856,6 +858,89 @@ const es = { } } +const pt = { + nav: { + timeline: 'Linha do tempo', + mentions: 'Menções', + public_tl: 'Linha do tempo pública', + twkn: 'Toda a rede conhecida' + }, + user_card: { + follows_you: 'Segue você!', + following: 'Seguindo!', + follow: 'Seguir', + blocked: 'Bloqueado!', + block: 'Bloquear', + statuses: 'Postagens', + mute: 'Mutar', + muted: 'Mudo', + followers: 'Seguidores', + followees: 'Seguindo', + per_day: 'por dia' + }, + timeline: { + show_new: 'Mostrar novas', + error_fetching: 'Erro buscando atualizações', + up_to_date: 'Atualizado', + load_older: 'Carregar postagens antigas', + conversation: 'Conversa' + }, + settings: { + user_settings: 'Configurações de Usuário', + name_bio: 'Nome & Biografia', + name: 'Nome', + bio: 'Biografia', + avatar: 'Avatar', + current_avatar: 'Seu avatar atual', + set_new_avatar: 'Mudar avatar', + profile_banner: 'Capa de perfil', + current_profile_banner: 'Sua capa de perfil atual', + set_new_profile_banner: 'Mudar capa de perfil', + profile_background: 'Plano de fundo de perfil', + set_new_profile_background: 'Mudar o plano de fundo de perfil', + settings: 'Configurações', + theme: 'Tema', + filtering: 'Filtragem', + filtering_explanation: 'Todas as postagens contendo estas palavras serão silenciadas, uma por linha.', + attachments: 'Anexos', + hide_attachments_in_tl: 'Ocultar anexos na linha do tempo.', + hide_attachments_in_convo: 'Ocultar anexos em conversas', + nsfw_clickthrough: 'Habilitar clique para ocultar anexos NSFW', + autoload: 'Habilitar carregamento automático quando a rolagem chegar ao fim.', + reply_link_preview: 'Habilitar a pré-visualização de link de respostas ao passar o mouse.' + }, + notifications: { + notifications: 'Notificações', + read: 'Ler!', + followed_you: 'seguiu você' + }, + login: { + login: 'Entrar', + username: 'Usuário', + password: 'Senha', + register: 'Registrar', + logout: 'Sair' + }, + registration: { + registration: 'Registro', + fullname: 'Nome para exibição', + email: 'Correio eletônico', + bio: 'Biografia', + password_confirm: 'Confirmação de senha' + }, + post_status: { + posting: 'Publicando', + default: 'Acabo de aterrizar em L.A.' + }, + finder: { + find_user: 'Buscar usuário', + error_fetching_user: 'Erro procurando usuário' + }, + general: { + submit: 'Enviar' + } +} + const messages = { de, fi, @@ -867,7 +952,8 @@ const messages = { fr, it, pl, - es + es, + pt } export default messages diff --git a/src/main.js b/src/main.js index 9e0b9e7c..87a46ee9 100644 --- a/src/main.js +++ b/src/main.js @@ -45,6 +45,7 @@ const persistedStateOptions = { 'config.hideNsfw', 'config.autoLoad', 'config.hoverPreview', + 'config.streaming', 'config.muteWords', 'users.lastLoginName' ] diff --git a/src/modules/config.js b/src/modules/config.js index f7d6e9c8..ac17747e 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -8,6 +8,7 @@ const defaultState = { hideAttachmentsInConv: false, hideNsfw: true, autoLoad: true, + streaming: false, hoverPreview: true, muteWords: [] } diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 5f2f8152..6d4b4843 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -131,7 +131,7 @@ export const statusType = (status) => { return 'favorite' } - if (status.text.match(/deleted notice {{tag/)) { + if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) { return 'deletion' } @@ -211,8 +211,10 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us sortTimeline(mentions) } - - addNotification({ type: 'mention', status, action: status }) + // Don't add notification for self-mention + if (status.user.id !== user.id) { + addNotification({ type: 'mention', status, action: status }) + } } } @@ -239,6 +241,25 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us // 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}) + + if ('Notification' in window && window.Notification.permission === 'granted') { + const title = action.user.name + const result = {} + result.icon = action.user.profile_image_url + result.body = action.text // there's a problem that it doesn't put a space before links tho + + // Shows first attached non-nsfw image, if any. Should add configuration for this somehow... + if (action.attachments.length > 0 && !action.nsfw && + action.attachments[0].mimetype.startsWith('image/')) { + result.image = action.attachments[0].url + } + + let notification = new window.Notification(title, result) + + // Chrome is known for not closing notifications automatically + // according to MDN, anyway. + setTimeout(notification.close.bind(notification), 5000) + } } } diff --git a/src/modules/users.js b/src/modules/users.js index f29cbf98..c8b6f0de 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -102,6 +102,10 @@ const users = { store.commit('addNewUsers', mutedUsers) }) + if ('Notification' in window && window.Notification.permission === 'default') { + window.Notification.requestPermission() + } + // Fetch our friends store.rootState.api.backendInteractor.fetchFriends() .then((friends) => commit('addNewUsers', friends)) |
