Merge remote-tracking branch 'upstream/develop' into feature/theming2

* upstream/develop:
  Fix iOS Safari from making videos play fullscreen by default
  added PR comments
  resolved the lint
  used the deleted data param as condition in status template
  Switch to "timeline" when pressing user-settings
  Added user setting tooltip
  made links in user bio always open in new tabs
  addressed PR comments
  added tooltip
  Add userId property to timelines so that we don't overwrite user timeline meant for another user
  Added option to auto-hide subject field when it's empty.
  removes hacks from notifications storage, adds api call to let server update is_seen attribute
  fixes vimium not giving retweet button a hint
  Do not use underscore at the beginning of the method
  Logout user on password change
  Route user to the correct profile URL
  Typo
  Fix filetype detection
  Switch to settings when touching settings
  Switch to timeline on nav panel actions
This commit is contained in:
Henry Jameson 2018-12-05 10:43:03 +03:00
commit 51cf4dc298
35 changed files with 134 additions and 52 deletions

View File

@ -11,7 +11,7 @@
</div> </div>
<div class='item right'> <div class='item right'>
<user-finder class="nav-icon"></user-finder> <user-finder class="nav-icon"></user-finder>
<router-link :to="{ name: 'settings'}"><i class="icon-cog nav-icon"></i></router-link> <router-link @click.native="activatePanel('timeline')" :to="{ name: 'settings'}"><i class="icon-cog nav-icon" :title="$t('nav.preferences')"></i></router-link>
<a href="#" v-if="currentUser" @click.prevent="logout"><i class="icon-logout nav-icon" :title="$t('login.logout')"></i></a> <a href="#" v-if="currentUser" @click.prevent="logout"><i class="icon-logout nav-icon" :title="$t('login.logout')"></i></a>
</div> </div>
</div> </div>
@ -25,8 +25,8 @@
<div class="sidebar-bounds"> <div class="sidebar-bounds">
<div class="sidebar-scroller"> <div class="sidebar-scroller">
<div class="sidebar"> <div class="sidebar">
<user-panel></user-panel> <user-panel :activatePanel="activatePanel"></user-panel>
<nav-panel></nav-panel> <nav-panel :activatePanel="activatePanel"></nav-panel>
<instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel> <instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
<features-panel v-if="!currentUser"></features-panel> <features-panel v-if="!currentUser"></features-panel>
<who-to-follow-panel v-if="currentUser && suggestionsEnabled"></who-to-follow-panel> <who-to-follow-panel v-if="currentUser && suggestionsEnabled"></who-to-follow-panel>

View File

@ -14,7 +14,7 @@
<StillImage :class="{'small': isSmall}" referrerpolicy="no-referrer" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/> <StillImage :class="{'small': isSmall}" referrerpolicy="no-referrer" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
</a> </a>
<video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" @loadeddata="onVideoDataLoad" :src="attachment.url" controls :loop="loopVideo"></video> <video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" @loadeddata="onVideoDataLoad" :src="attachment.url" controls :loop="loopVideo" playsinline></video>
<audio v-if="type === 'audio'" :src="attachment.url" controls></audio> <audio v-if="type === 'audio'" :src="attachment.url" controls></audio>

View File

@ -1,10 +1,10 @@
<template> <template>
<div v-if="loggedIn"> <div v-if="loggedIn">
<i :class='classes' class='favorite-button fav-active' @click.prevent='favorite()'/> <i :class='classes' class='favorite-button fav-active' @click.prevent='favorite()' :title="$t('tool_tip.favorite')"/>
<span v-if='!hidePostStatsLocal && status.fave_num > 0'>{{status.fave_num}}</span> <span v-if='!hidePostStatsLocal && status.fave_num > 0'>{{status.fave_num}}</span>
</div> </div>
<div v-else> <div v-else>
<i :class='classes' class='favorite-button'/> <i :class='classes' class='favorite-button' :title="$t('tool_tip.favorite')"/>
<span v-if='!hidePostStatsLocal && status.fave_num > 0'>{{status.fave_num}}</span> <span v-if='!hidePostStatsLocal && status.fave_num > 0'>{{status.fave_num}}</span>
</div> </div>
</template> </template>

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="media-upload" @drop.prevent @dragover.prevent="fileDrag" @drop="fileDrop"> <div class="media-upload" @drop.prevent @dragover.prevent="fileDrag" @drop="fileDrop">
<label class="btn btn-default"> <label class="btn btn-default" :title="$t('tool_tip.media_upload')">
<i class="icon-spin4 animate-spin" v-if="uploading"></i> <i class="icon-spin4 animate-spin" v-if="uploading"></i>
<i class="icon-upload" v-if="!uploading"></i> <i class="icon-upload" v-if="!uploading"></i>
<input type="file" style="position: fixed; top: -100em" multiple="true"></input> <input type="file" style="position: fixed; top: -100em" multiple="true"></input>

View File

@ -1,4 +1,5 @@
const NavPanel = { const NavPanel = {
props: [ 'activatePanel' ],
computed: { computed: {
currentUser () { currentUser () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser

View File

@ -3,32 +3,32 @@
<div class="panel panel-default"> <div class="panel panel-default">
<ul> <ul>
<li v-if='currentUser'> <li v-if='currentUser'>
<router-link to='/main/friends'> <router-link @click.native="activatePanel('timeline')" to='/main/friends'>
{{ $t("nav.timeline") }} {{ $t("nav.timeline") }}
</router-link> </router-link>
</li> </li>
<li v-if='currentUser'> <li v-if='currentUser'>
<router-link :to="{ name: 'mentions', params: { username: currentUser.screen_name } }"> <router-link @click.native="activatePanel('timeline')" :to="{ name: 'mentions', params: { username: currentUser.screen_name } }">
{{ $t("nav.mentions") }} {{ $t("nav.mentions") }}
</router-link> </router-link>
</li> </li>
<li v-if='currentUser'> <li v-if='currentUser'>
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> <router-link @click.native="activatePanel('timeline')" :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
{{ $t("nav.dms") }} {{ $t("nav.dms") }}
</router-link> </router-link>
</li> </li>
<li v-if='currentUser && currentUser.locked'> <li v-if='currentUser && currentUser.locked'>
<router-link to='/friend-requests'> <router-link @click.native="activatePanel('timeline')" to='/friend-requests'>
{{ $t("nav.friend_requests") }} {{ $t("nav.friend_requests") }}
</router-link> </router-link>
</li> </li>
<li> <li>
<router-link to='/main/public'> <router-link @click.native="activatePanel('timeline')" to='/main/public'>
{{ $t("nav.public_tl") }} {{ $t("nav.public_tl") }}
</router-link> </router-link>
</li> </li>
<li> <li>
<router-link to='/main/all'> <router-link @click.native="activatePanel('timeline')" to='/main/all'>
{{ $t("nav.twkn") }} {{ $t("nav.twkn") }}
</router-link> </router-link>
</li> </li>

View File

@ -17,7 +17,7 @@
<small>{{$t('notifications.favorited_you')}}</small> <small>{{$t('notifications.favorited_you')}}</small>
</span> </span>
<span v-if="notification.type === 'repeat'"> <span v-if="notification.type === 'repeat'">
<i class="fa icon-retweet lit"></i> <i class="fa icon-retweet lit" :title="$t('tool_tip.repeat')"></i>
<small>{{$t('notifications.repeated_you')}}</small> <small>{{$t('notifications.repeated_you')}}</small>
</span> </span>
<span v-if="notification.type === 'follow'"> <span v-if="notification.type === 'follow'">

View File

@ -52,7 +52,7 @@ const Notifications = {
}, },
methods: { methods: {
markAsSeen () { markAsSeen () {
this.$store.commit('markNotificationsAsSeen', this.visibleNotifications) this.$store.dispatch('markNotificationsAsSeen', this.visibleNotifications)
}, },
fetchOlderNotifications () { fetchOlderNotifications () {
const store = this.$store const store = this.$store

View File

@ -150,6 +150,15 @@ const PostStatusForm = {
scopeOptionsEnabled () { scopeOptionsEnabled () {
return this.$store.state.instance.scopeOptionsEnabled return this.$store.state.instance.scopeOptionsEnabled
}, },
alwaysShowSubject () {
if (typeof this.$store.state.config.alwaysShowSubjectInput !== 'undefined') {
return this.$store.state.config.alwaysShowSubjectInput
} else if (typeof this.$store.state.instance.alwaysShowSubjectInput !== 'undefined') {
return this.$store.state.instance.alwaysShowSubjectInput
} else {
return this.$store.state.instance.scopeOptionsEnabled
}
},
formattingOptionsEnabled () { formattingOptionsEnabled () {
return this.$store.state.instance.formattingOptionsEnabled return this.$store.state.instance.formattingOptionsEnabled
} }

View File

@ -11,7 +11,7 @@
</i18n> </i18n>
<p v-if="this.newStatus.visibility == 'direct'" class="visibility-notice">{{ $t('post_status.direct_warning') }}</p> <p v-if="this.newStatus.visibility == 'direct'" class="visibility-notice">{{ $t('post_status.direct_warning') }}</p>
<input <input
v-if="scopeOptionsEnabled" v-if="newStatus.spoilerText || alwaysShowSubject"
type="text" type="text"
:placeholder="$t('post_status.content_warning')" :placeholder="$t('post_status.content_warning')"
v-model="newStatus.spoilerText" v-model="newStatus.spoilerText"

View File

@ -1,7 +1,7 @@
<template> <template>
<div v-if="loggedIn"> <div v-if="loggedIn">
<template v-if="visibility !== 'private' && visibility !== 'direct'"> <template v-if="visibility !== 'private' && visibility !== 'direct'">
<i :class='classes' class='icon-retweet rt-active' v-on:click.prevent='retweet()'></i> <i :class='classes' class='retweet-button icon-retweet rt-active' v-on:click.prevent='retweet()' :title="$t('tool_tip.repeat')"></i>
<span v-if='!hidePostStatsLocal && status.repeat_num > 0'>{{status.repeat_num}}</span> <span v-if='!hidePostStatsLocal && status.repeat_num > 0'>{{status.repeat_num}}</span>
</template> </template>
<template v-else> <template v-else>
@ -9,7 +9,7 @@
</template> </template>
</div> </div>
<div v-else-if="!loggedIn"> <div v-else-if="!loggedIn">
<i :class='classes' class='icon-retweet'></i> <i :class='classes' class='icon-retweet' :title="$t('tool_tip.repeat')"></i>
<span v-if='!hidePostStatsLocal && status.repeat_num > 0'>{{status.repeat_num}}</span> <span v-if='!hidePostStatsLocal && status.repeat_num > 0'>{{status.repeat_num}}</span>
</div> </div>
</template> </template>

View File

@ -38,6 +38,10 @@ const settings = {
? instance.subjectLineBehavior ? instance.subjectLineBehavior
: user.subjectLineBehavior, : user.subjectLineBehavior,
subjectLineBehaviorDefault: instance.subjectLineBehavior, subjectLineBehaviorDefault: instance.subjectLineBehavior,
alwaysShowSubjectInputLocal: typeof user.alwaysShowSubjectInput === 'undefined'
? instance.alwaysShowSubjectInput
: user.alwaysShowSubjectInput,
alwaysShowSubjectInputDefault: instance.alwaysShowSubjectInput,
scopeCopyLocal: user.scopeCopy, scopeCopyLocal: user.scopeCopy,
scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy), scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy),
stopGifs: user.stopGifs, stopGifs: user.stopGifs,
@ -122,6 +126,9 @@ const settings = {
scopeCopyLocal (value) { scopeCopyLocal (value) {
this.$store.dispatch('setOption', { name: 'scopeCopy', value }) this.$store.dispatch('setOption', { name: 'scopeCopy', value })
}, },
alwaysShowSubjectInputLocal (value) {
this.$store.dispatch('setOption', { name: 'alwaysShowSubjectInput', value })
},
subjectLineBehaviorLocal (value) { subjectLineBehaviorLocal (value) {
this.$store.dispatch('setOption', { name: 'subjectLineBehavior', value }) this.$store.dispatch('setOption', { name: 'subjectLineBehavior', value })
}, },

View File

@ -64,6 +64,12 @@
{{$t('settings.scope_copy')}} {{$t('settings.instance_default', { value: scopeCopyDefault })}} {{$t('settings.scope_copy')}} {{$t('settings.instance_default', { value: scopeCopyDefault })}}
</label> </label>
</li> </li>
<li>
<input type="checkbox" id="subjectHide" v-model="alwaysShowSubjectInputLocal">
<label for="subjectHide">
{{$t('settings.subject_input_always_show')}} {{$t('settings.instance_default', { value: alwaysShowSubjectInputDefault })}}
</label>
</li>
<li> <li>
<div> <div>
{{$t('settings.subject_line_behavior')}} {{$t('settings.subject_line_behavior')}}

View File

@ -54,6 +54,9 @@ const Status = {
const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
return highlightClass(user) return highlightClass(user)
}, },
deleted () {
return this.statusoid.deleted
},
repeaterStyle () { repeaterStyle () {
const user = this.statusoid.user const user = this.statusoid.user
const highlight = this.$store.state.config.highlight const highlight = this.$store.state.config.highlight

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="status-el" v-if="!hideReply" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]"> <div class="status-el" v-if="!hideReply && !deleted" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
<template v-if="muted && !noReplyLinks"> <template v-if="muted && !noReplyLinks">
<div class="media status container 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> <small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
@ -13,7 +13,7 @@
<div class="media-body faint"> <div class="media-body faint">
<a v-if="retweeterHtml" :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a> <a v-if="retweeterHtml" :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a>
<a v-else :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a> <a v-else :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
<i class='fa icon-retweet retweeted'></i> <i class='fa icon-retweet retweeted' :title="$t('tool_tip.repeat')"></i>
{{$t('timeline.repeated')}} {{$t('timeline.repeated')}}
</div> </div>
</div> </div>
@ -41,7 +41,7 @@
{{status.in_reply_to_screen_name}} {{status.in_reply_to_screen_name}}
</router-link> </router-link>
</span> </span>
<a v-if="isReply && !noReplyLinks" href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)"> <a v-if="isReply && !noReplyLinks" href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)" :title="$t('tool_tip.reply')">
<i class="icon-reply" @mouseenter="replyEnter(status.in_reply_to_status_id, $event)" @mouseout="replyLeave()"></i> <i class="icon-reply" @mouseenter="replyEnter(status.in_reply_to_status_id, $event)" @mouseout="replyLeave()"></i>
</a> </a>
</span> </span>
@ -94,7 +94,7 @@
<div v-if="!noHeading && !noReplyLinks" class='status-actions media-body'> <div v-if="!noHeading && !noReplyLinks" class='status-actions media-body'>
<div v-if="loggedIn"> <div v-if="loggedIn">
<a href="#" v-on:click.prevent="toggleReplying"> <a href="#" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')">
<i class="icon-reply" :class="{'icon-reply-active': replying}"></i> <i class="icon-reply" :class="{'icon-reply-active': replying}"></i>
</a> </a>
</div> </div>

View File

@ -19,7 +19,9 @@
{{ $t('user_card.follows_you') }} {{ $t('user_card.follows_you') }}
</span> </span>
</div> </div>
<a :href="user.statusnet_profile_url" target="blank"><div class="user-screen-name">@{{ user.screen_name }}</div></a> <router-link class='user-screen-name' :to="{ name: 'user-profile', params: { id: user.id } }">
@{{user.screen_name}}
</router-link>
</div> </div>
<div class="approval" v-if="showApproval"> <div class="approval" v-if="showApproval">
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button> <button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>

View File

@ -2,7 +2,7 @@ import StillImage from '../still-image/still-image.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js' import { hex2rgb } from '../../services/color_convert/color_convert.js'
export default { export default {
props: [ 'user', 'switcher', 'selected', 'hideBio' ], props: [ 'user', 'switcher', 'selected', 'hideBio', 'activatePanel' ],
data () { data () {
return { return {
hideUserStatsLocal: typeof this.$store.state.config.hideUserStats === 'undefined' hideUserStatsLocal: typeof this.$store.state.config.hideUserStats === 'undefined'
@ -102,6 +102,14 @@ export default {
const store = this.$store const store = this.$store
store.commit('setProfileView', { v }) store.commit('setProfileView', { v })
} }
},
linkClicked ({target}) {
if (target.tagName === 'SPAN') {
target = target.parentNode
}
if (target.tagName === 'A') {
window.open(target.href, '_blank')
}
} }
} }
} }

View File

@ -2,8 +2,8 @@
<div id="heading" class="profile-panel-background" :style="headingStyle"> <div id="heading" class="profile-panel-background" :style="headingStyle">
<div class="panel-heading text-center"> <div class="panel-heading text-center">
<div class='user-info'> <div class='user-info'>
<router-link to='/user-settings' style="float: right; margin-top:16px;" v-if="!isOtherUser"> <router-link @click.native="activatePanel('timeline')" to='/user-settings' style="float: right; margin-top:16px;" v-if="!isOtherUser">
<i class="icon-cog usersettings"></i> <i class="icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i>
</router-link> </router-link>
<a :href="user.statusnet_profile_url" target="_blank" class="floater" v-if="isOtherUser"> <a :href="user.statusnet_profile_url" target="_blank" class="floater" v-if="isOtherUser">
<i class="icon-link-ext usersettings"></i> <i class="icon-link-ext usersettings"></i>
@ -105,7 +105,7 @@
<span v-if="!hideUserStatsLocal">{{user.followers_count}}</span> <span v-if="!hideUserStatsLocal">{{user.followers_count}}</span>
</div> </div>
</div> </div>
<p v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p> <p @click.prevent="linkClicked" v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p>
<p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p> <p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p>
</div> </div>
</div> </div>

View File

@ -1,7 +1,7 @@
<template> <template>
<span class="user-finder-container"> <span class="user-finder-container">
<i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" /> <i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
<a href="#" v-if="hidden"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden"/></a> <a href="#" v-if="hidden" :title="$t('finder.find_user')" ><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
<span v-else> <span v-else>
<input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/> <input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
<i class="icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/> <i class="icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>

View File

@ -3,6 +3,7 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCardContent from '../user_card_content/user_card_content.vue' import UserCardContent from '../user_card_content/user_card_content.vue'
const UserPanel = { const UserPanel = {
props: [ 'activatePanel' ],
computed: { computed: {
user () { return this.$store.state.users.currentUser } user () { return this.$store.state.users.currentUser }
}, },

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="user-panel"> <div class="user-panel">
<div v-if='user' class="panel panel-default" style="overflow: visible;"> <div v-if='user' class="panel panel-default" style="overflow: visible;">
<user-card-content :user="user" :switcher="false" :hideBio="true"></user-card-content> <user-card-content :activatePanel="activatePanel" :user="user" :switcher="false" :hideBio="true"></user-card-content>
<div class="panel-footer"> <div class="panel-footer">
<post-status-form v-if='user'></post-status-form> <post-status-form v-if='user'></post-status-form>
</div> </div>

View File

@ -27,6 +27,7 @@ const UserProfile = {
}, },
watch: { watch: {
userId () { userId () {
this.$store.dispatch('stopFetching', 'user')
this.$store.commit('clearTimeline', { timeline: 'user' }) this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.dispatch('startFetching', ['user', this.userId]) this.$store.dispatch('startFetching', ['user', this.userId])
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="user-seach panel panel-default"> <div class="user-search panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
{{$t('nav.user_search')}} {{$t('nav.user_search')}}
</div> </div>

View File

@ -235,6 +235,7 @@ const UserSettings = {
if (res.status === 'success') { if (res.status === 'success') {
this.changedPassword = true this.changedPassword = true
this.changePasswordError = false this.changePasswordError = false
this.logout()
} else { } else {
this.changedPassword = false this.changedPassword = false
this.changePasswordError = res.error this.changePasswordError = res.error
@ -243,6 +244,10 @@ const UserSettings = {
}, },
activateTab (tabName) { activateTab (tabName) {
this.activeTab = tabName this.activeTab = tabName
},
logout () {
this.$store.dispatch('logout')
this.$router.replace('/')
} }
} }
} }

View File

@ -19,10 +19,10 @@
<div v-if="scopeOptionsEnabled"> <div v-if="scopeOptionsEnabled">
<label for="default-vis">{{$t('settings.default_vis')}}</label> <label for="default-vis">{{$t('settings.default_vis')}}</label>
<div id="default-vis" class="visibility-tray"> <div id="default-vis" class="visibility-tray">
<i v-on:click="changeVis('direct')" class="icon-mail-alt" :class="vis.direct"></i> <i v-on:click="changeVis('direct')" class="icon-mail-alt" :class="vis.direct" :title="$t('post_status.scope.direct')" ></i>
<i v-on:click="changeVis('private')" class="icon-lock" :class="vis.private"></i> <i v-on:click="changeVis('private')" class="icon-lock" :class="vis.private" :title="$t('post_status.scope.private')"></i>
<i v-on:click="changeVis('unlisted')" class="icon-lock-open-alt" :class="vis.unlisted"></i> <i v-on:click="changeVis('unlisted')" class="icon-lock-open-alt" :class="vis.unlisted" :title="$t('post_status.scope.unlisted')"></i>
<i v-on:click="changeVis('public')" class="icon-globe" :class="vis.public"></i> <i v-on:click="changeVis('public')" class="icon-globe" :class="vis.public" :title="$t('post_status.scope.public')"></i>
</div> </div>
</div> </div>
<p> <p>

View File

@ -36,7 +36,8 @@
"public_tl": "Public Timeline", "public_tl": "Public Timeline",
"timeline": "Timeline", "timeline": "Timeline",
"twkn": "The Whole Known Network", "twkn": "The Whole Known Network",
"user_search": "User Search" "user_search": "User Search",
"preferences": "Preferences"
}, },
"notifications": { "notifications": {
"broken_favorite": "Unknown status, searching for it...", "broken_favorite": "Unknown status, searching for it...",
@ -160,6 +161,7 @@
"set_new_profile_background": "Set new profile background", "set_new_profile_background": "Set new profile background",
"set_new_profile_banner": "Set new profile banner", "set_new_profile_banner": "Set new profile banner",
"settings": "Settings", "settings": "Settings",
"subject_input_always_show": "Always show subject field",
"subject_line_behavior": "Copy subject when replying", "subject_line_behavior": "Copy subject when replying",
"subject_line_email": "Like email: \"re: subject\"", "subject_line_email": "Like email: \"re: subject\"",
"subject_line_mastodon": "Like mastodon: copy as is", "subject_line_mastodon": "Like mastodon: copy as is",
@ -318,5 +320,12 @@
"who_to_follow": { "who_to_follow": {
"more": "More", "more": "More",
"who_to_follow": "Who to follow" "who_to_follow": "Who to follow"
},
"tool_tip": {
"media_upload": "Upload Media",
"repeat": "Repeat",
"reply": "Reply",
"favorite": "Favorite",
"user_settings": "User Settings"
} }
} }

View File

@ -134,6 +134,7 @@
"set_new_profile_background": "Загрузить новый фон профиля", "set_new_profile_background": "Загрузить новый фон профиля",
"set_new_profile_banner": "Загрузить новый баннер профиля", "set_new_profile_banner": "Загрузить новый баннер профиля",
"settings": "Настройки", "settings": "Настройки",
"subject_input_always_show": "Всегда показывать поле ввода темы",
"stop_gifs": "Проигрывать GIF анимации только при наведении", "stop_gifs": "Проигрывать GIF анимации только при наведении",
"streaming": "Включить автоматическую загрузку новых сообщений при прокрутке вверх", "streaming": "Включить автоматическую загрузку новых сообщений при прокрутке вверх",
"text": "Текст", "text": "Текст",

View File

@ -47,7 +47,6 @@ const persistedStateOptions = {
paths: [ paths: [
'config', 'config',
'users.lastLoginName', 'users.lastLoginName',
'statuses.notifications.maxSavedId',
'oauth' 'oauth'
] ]
} }

View File

@ -27,7 +27,8 @@ const defaultState = {
highlight: {}, highlight: {},
interfaceLanguage: browserLocale, interfaceLanguage: browserLocale,
scopeCopy: undefined, // instance default scopeCopy: undefined, // instance default
subjectLineBehavior: undefined // instance default subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined // instance default
} }
const config = { const config = {

View File

@ -17,6 +17,7 @@ const defaultState = {
showInstanceSpecificPanel: false, showInstanceSpecificPanel: false,
scopeOptionsEnabled: true, scopeOptionsEnabled: true,
formattingOptionsEnabled: false, formattingOptionsEnabled: false,
alwaysShowSubjectInput: true,
collapseMessageWithSubject: false, collapseMessageWithSubject: false,
hidePostStats: false, hidePostStats: false,
hideUserStats: false, hideUserStats: false,

View File

@ -1,5 +1,4 @@
import { includes, remove, slice, sortBy, toInteger, each, find, flatten, maxBy, minBy, merge, last, isArray } from 'lodash' import { includes, remove, slice, sortBy, toInteger, each, find, flatten, maxBy, minBy, merge, last, isArray } from 'lodash'
import { set } from 'vue'
import apiService from '../services/api/api.service.js' import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js' // import parse from '../services/status_parser/status_parser.js'
@ -16,6 +15,7 @@ const emptyTl = () => ({
followers: [], followers: [],
friends: [], friends: [],
viewing: 'statuses', viewing: 'statuses',
userId: 0,
flushMarker: 0 flushMarker: 0
}) })
@ -26,7 +26,6 @@ export const defaultState = {
notifications: { notifications: {
desktopNotificationSilence: true, desktopNotificationSilence: true,
maxId: 0, maxId: 0,
maxSavedId: 0,
minId: Number.POSITIVE_INFINITY, minId: Number.POSITIVE_INFINITY,
data: [], data: [],
error: false, error: false,
@ -132,7 +131,7 @@ const sortTimeline = (timeline) => {
return timeline return timeline
} }
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false }) => { const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId }) => {
// Sanity check // Sanity check
if (!isArray(statuses)) { if (!isArray(statuses)) {
return false return false
@ -149,6 +148,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
timelineObject.maxId = maxNew timelineObject.maxId = maxNew
} }
// This makes sure that user timeline won't get data meant for other
// user. I.e. opening different user profiles makes request which could
// return data late after user already viewing different user profile
if (timeline === 'user' && timelineObject.userId !== userId) {
return
}
const addStatus = (status, showImmediately, addToTimeline = true) => { const addStatus = (status, showImmediately, addToTimeline = true) => {
const result = mergeOrAdd(allStatuses, allStatusesObject, status) const result = mergeOrAdd(allStatuses, allStatusesObject, status)
status = result.item status = result.item
@ -297,7 +303,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
state.notifications.maxId = Math.max(notification.id, state.notifications.maxId) state.notifications.maxId = Math.max(notification.id, state.notifications.maxId)
state.notifications.minId = Math.min(notification.id, state.notifications.minId) state.notifications.minId = Math.min(notification.id, state.notifications.minId)
const fresh = !older && !notification.is_seen && notification.id > state.notifications.maxSavedId const fresh = !notification.is_seen
const status = notification.ntype === 'like' const status = notification.ntype === 'like'
? find(allStatuses, { id: action.in_reply_to_status_id }) ? find(allStatuses, { id: action.in_reply_to_status_id })
: action : action
@ -306,7 +312,6 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
type: notification.ntype, type: notification.ntype,
status, status,
action, action,
// Always assume older notifications as seen
seen: !fresh seen: !fresh
} }
@ -404,9 +409,8 @@ export const mutations = {
addFollowers (state, { followers }) { addFollowers (state, { followers }) {
state.timelines['user'].followers = followers state.timelines['user'].followers = followers
}, },
markNotificationsAsSeen (state, notifications) { markNotificationsAsSeen (state) {
set(state.notifications, 'maxSavedId', state.notifications.maxId) each(state.notifications.data, (notification) => {
each(notifications, (notification) => {
notification.seen = true notification.seen = true
}) })
}, },
@ -418,8 +422,8 @@ export const mutations = {
const statuses = { const statuses = {
state: defaultState, state: defaultState,
actions: { actions: {
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false }) { addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) {
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser }) commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId })
}, },
addNewNotifications ({ rootState, commit, dispatch }, { notifications, older }) { addNewNotifications ({ rootState, commit, dispatch }, { notifications, older }) {
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older }) commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older })
@ -484,6 +488,13 @@ const statuses = {
}, },
queueFlush ({ rootState, commit }, { timeline, id }) { queueFlush ({ rootState, commit }, { timeline, id }) {
commit('queueFlush', { timeline, id }) commit('queueFlush', { timeline, id })
},
markNotificationsAsSeen ({ rootState, commit }) {
commit('markNotificationsAsSeen')
apiService.markNotificationsAsSeen({
id: rootState.statuses.notifications.maxId,
credentials: rootState.users.currentUser.credentials
})
} }
}, },
mutations mutations

View File

@ -29,6 +29,7 @@ const PROFILE_UPDATE_URL = '/api/account/update_profile.json'
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json' const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
const QVITTER_USER_TIMELINE_URL = '/api/qvitter/statuses/user_timeline.json' const QVITTER_USER_TIMELINE_URL = '/api/qvitter/statuses/user_timeline.json'
const QVITTER_USER_NOTIFICATIONS_URL = '/api/qvitter/statuses/notifications.json' const QVITTER_USER_NOTIFICATIONS_URL = '/api/qvitter/statuses/notifications.json'
const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
const BLOCKING_URL = '/api/blocks/create.json' const BLOCKING_URL = '/api/blocks/create.json'
const UNBLOCKING_URL = '/api/blocks/destroy.json' const UNBLOCKING_URL = '/api/blocks/destroy.json'
const USER_URL = '/api/users/show.json' const USER_URL = '/api/users/show.json'
@ -460,6 +461,18 @@ const suggestions = ({credentials}) => {
}).then((data) => data.json()) }).then((data) => data.json())
} }
const markNotificationsAsSeen = ({id, credentials}) => {
const body = new FormData()
body.append('latest_id', id)
return fetch(QVITTER_USER_NOTIFICATIONS_READ_URL, {
body,
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const apiService = { const apiService = {
verifyCredentials, verifyCredentials,
fetchTimeline, fetchTimeline,
@ -494,7 +507,8 @@ const apiService = {
fetchFollowRequests, fetchFollowRequests,
approveUser, approveUser,
denyUser, denyUser,
suggestions suggestions,
markNotificationsAsSeen
} }
export default apiService export default apiService

View File

@ -9,11 +9,11 @@ const fileType = (typeString) => {
type = 'image' type = 'image'
} }
if (typeString.match(/video\/(webm|mp4)/)) { if (typeString.match(/video/)) {
type = 'video' type = 'video'
} }
if (typeString.match(/audio|ogg/)) { if (typeString.match(/audio/)) {
type = 'audio' type = 'audio'
} }

View File

@ -2,13 +2,14 @@ import { camelCase } from 'lodash'
import apiService from '../api/api.service.js' import apiService from '../api/api.service.js'
const update = ({store, statuses, timeline, showImmediately}) => { const update = ({store, statuses, timeline, showImmediately, userId}) => {
const ccTimeline = camelCase(timeline) const ccTimeline = camelCase(timeline)
store.dispatch('setError', { value: false }) store.dispatch('setError', { value: false })
store.dispatch('addNewStatuses', { store.dispatch('addNewStatuses', {
timeline: ccTimeline, timeline: ccTimeline,
userId,
statuses, statuses,
showImmediately showImmediately
}) })
@ -33,7 +34,7 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
if (!older && statuses.length >= 20 && !timelineData.loading) { if (!older && statuses.length >= 20 && !timelineData.loading) {
store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId }) store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId })
} }
update({store, statuses, timeline, showImmediately}) update({store, statuses, timeline, showImmediately, userId})
}, () => store.dispatch('setError', { value: true })) }, () => store.dispatch('setError', { value: true }))
} }
@ -41,6 +42,7 @@ const startFetching = ({timeline = 'friends', credentials, store, userId = false
const rootState = store.rootState || store.state const rootState = store.rootState || store.state
const timelineData = rootState.statuses.timelines[camelCase(timeline)] const timelineData = rootState.statuses.timelines[camelCase(timeline)]
const showImmediately = timelineData.visibleStatuses.length === 0 const showImmediately = timelineData.visibleStatuses.length === 0
timelineData.userId = userId
fetchAndUpdate({timeline, credentials, store, showImmediately, userId, tag}) fetchAndUpdate({timeline, credentials, store, showImmediately, userId, tag})
const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store, userId, tag }) const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store, userId, tag })
return setInterval(boundFetchAndUpdate, 10000) return setInterval(boundFetchAndUpdate, 10000)

View File

@ -13,6 +13,7 @@
"collapseMessageWithSubject": false, "collapseMessageWithSubject": false,
"scopeCopy": false, "scopeCopy": false,
"subjectLineBehavior": "email", "subjectLineBehavior": "email",
"alwaysShowSubjectInput": true,
"hidePostStats": false, "hidePostStats": false,
"hideUserStats": false, "hideUserStats": false,
"loginMethod": "password" "loginMethod": "password"