Merge remote-tracking branch 'upstream/develop' into shigusegubu
* upstream/develop: (25 commits) improve notification subscription Fix typo that prevented scope copy from working. added check for activatePanel is function or not addressed PR comments activate panel on user screen click added not preload check so hidden toggles asap removed counters from left panel added router-links to all relavent links added activatePanel onclick for timeago button added PR comments add checkbox to disable web push removed brackets from condition resolved lint issue renamed config to preload images and add ident to config added config for preload and made attachment responsive to it preload nsfw image fix race condition improve push notifications code second attempt to add subscribe module and fix race condition Revert "add subscribe module and fix race condition" ...
This commit is contained in:
commit
b91420b17e
24 changed files with 253 additions and 42 deletions
|
@ -2,6 +2,7 @@ var path = require('path')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var utils = require('./utils')
|
var utils = require('./utils')
|
||||||
var projectRoot = path.resolve(__dirname, '../')
|
var projectRoot = path.resolve(__dirname, '../')
|
||||||
|
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
|
||||||
|
|
||||||
var env = process.env.NODE_ENV
|
var env = process.env.NODE_ENV
|
||||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||||
|
@ -91,5 +92,10 @@ module.exports = {
|
||||||
browsers: ['last 2 versions']
|
browsers: ['last 2 versions']
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
plugins: [
|
||||||
|
new ServiceWorkerWebpackPlugin({
|
||||||
|
entry: path.join(__dirname, '..', 'src/sw.js')
|
||||||
|
})
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,6 +90,7 @@
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"selenium-server": "2.53.1",
|
"selenium-server": "2.53.1",
|
||||||
"semver": "^5.3.0",
|
"semver": "^5.3.0",
|
||||||
|
"serviceworker-webpack-plugin": "0.2.3",
|
||||||
"shelljs": "^0.7.4",
|
"shelljs": "^0.7.4",
|
||||||
"sinon": "^1.17.3",
|
"sinon": "^1.17.3",
|
||||||
"sinon-chai": "^2.8.0",
|
"sinon-chai": "^2.8.0",
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<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>
|
||||||
<notifications v-if="currentUser"></notifications>
|
<notifications :activatePanel="activatePanel" v-if="currentUser"></notifications>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,13 +21,17 @@ const afterStoreSetup = ({store, i18n}) => {
|
||||||
window.fetch('/api/statusnet/config.json')
|
window.fetch('/api/statusnet/config.json')
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
const {name, closed: registrationClosed, textlimit, server} = data.site
|
const { name, closed: registrationClosed, textlimit, server, vapidPublicKey } = data.site
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', { name: 'name', value: name })
|
store.dispatch('setInstanceOption', { name: 'name', value: name })
|
||||||
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
|
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
|
||||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
|
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
|
||||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||||
|
|
||||||
|
if (vapidPublicKey) {
|
||||||
|
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||||
|
}
|
||||||
|
|
||||||
var apiConfig = data.site.pleromafe
|
var apiConfig = data.site.pleromafe
|
||||||
|
|
||||||
window.fetch('/static/config.json')
|
window.fetch('/static/config.json')
|
||||||
|
|
|
@ -13,6 +13,7 @@ const Attachment = {
|
||||||
return {
|
return {
|
||||||
nsfwImage,
|
nsfwImage,
|
||||||
hideNsfwLocal: this.$store.state.config.hideNsfw,
|
hideNsfwLocal: this.$store.state.config.hideNsfw,
|
||||||
|
preloadImage: this.$store.state.config.preloadImage,
|
||||||
loopVideo: this.$store.state.config.loopVideo,
|
loopVideo: this.$store.state.config.loopVideo,
|
||||||
showHidden: false,
|
showHidden: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -46,7 +47,7 @@ const Attachment = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleHidden () {
|
toggleHidden () {
|
||||||
if (this.img) {
|
if (this.img && !this.preloadImage) {
|
||||||
if (this.img.onload) {
|
if (this.img.onload) {
|
||||||
this.img.onload()
|
this.img.onload()
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -9,8 +9,7 @@
|
||||||
<div class="hider" v-if="nsfw && hideNsfwLocal && !hidden">
|
<div class="hider" v-if="nsfw && hideNsfwLocal && !hidden">
|
||||||
<a href="#" @click.prevent="toggleHidden()">Hide</a>
|
<a href="#" @click.prevent="toggleHidden()">Hide</a>
|
||||||
</div>
|
</div>
|
||||||
|
<a v-if="type === 'image' && (!hidden || preloadImage)" class="image-attachment" :class="{'hidden': hidden && preloadImage}" :href="attachment.url" target="_blank" :title="attachment.description">
|
||||||
<a v-if="type === 'image' && !hidden" class="image-attachment" :href="attachment.url" target="_blank" :title="attachment.description">
|
|
||||||
<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>
|
||||||
|
|
||||||
|
@ -161,6 +160,10 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
|
&.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.still-image {
|
.still-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -11,7 +11,8 @@ const Notification = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
'notification'
|
'notification',
|
||||||
|
'activatePanel'
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
Status, StillImage, UserCardContent
|
Status, StillImage, UserCardContent
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
|
<status :activatePanel="activatePanel" v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
|
||||||
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
|
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
|
||||||
<a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
|
<a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
|
||||||
<StillImage class='avatar-compact' :class="{'better-shadow': betterShadow}" :src="notification.action.user.profile_image_url_original"/>
|
<StillImage class='avatar-compact' :class="{'better-shadow': betterShadow}" :src="notification.action.user.profile_image_url_original"/>
|
||||||
|
@ -25,13 +25,13 @@
|
||||||
<small>{{$t('notifications.followed_you')}}</small>
|
<small>{{$t('notifications.followed_you')}}</small>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<small class="timeago"><router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
|
<small class="timeago"><router-link @click.native="activatePanel('timeline')" v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
|
||||||
</span>
|
</span>
|
||||||
<div class="follow-text" v-if="notification.type === 'follow'">
|
<div class="follow-text" v-if="notification.type === 'follow'">
|
||||||
<router-link :to="{ name: 'user-profile', params: { id: notification.action.user.id } }">@{{notification.action.user.screen_name}}</router-link>
|
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: notification.action.user.id } }">@{{notification.action.user.screen_name}}</router-link>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<status v-if="notification.status" class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
|
<status :activatePanel="activatePanel" v-if="notification.status" class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
|
||||||
<div class="broken-favorite" v-else>
|
<div class="broken-favorite" v-else>
|
||||||
{{$t('notifications.broken_favorite')}}
|
{{$t('notifications.broken_favorite')}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import notificationsFetcher from '../../services/notifications_fetcher/notificat
|
||||||
import { sortBy, filter } from 'lodash'
|
import { sortBy, filter } from 'lodash'
|
||||||
|
|
||||||
const Notifications = {
|
const Notifications = {
|
||||||
|
props: [ 'activatePanel' ],
|
||||||
created () {
|
created () {
|
||||||
const store = this.$store
|
const store = this.$store
|
||||||
const credentials = store.state.users.currentUser.credentials
|
const credentials = store.state.users.currentUser.credentials
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'>
|
<div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'>
|
||||||
<div class="notification-overlay"></div>
|
<div class="notification-overlay"></div>
|
||||||
<notification :notification="notification"></notification>
|
<notification :activatePanel="activatePanel" :notification="notification"></notification>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-footer">
|
<div class="panel-footer">
|
||||||
|
|
|
@ -46,7 +46,7 @@ const PostStatusForm = {
|
||||||
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
const scope = (this.copyMessageScope && this.$store.state.config.copyScope || this.copyMessageScope === 'direct')
|
const scope = (this.copyMessageScope && this.$store.state.config.scopeCopy || this.copyMessageScope === 'direct')
|
||||||
? this.copyMessageScope
|
? this.copyMessageScope
|
||||||
: this.$store.state.users.currentUser.default_scope
|
: this.$store.state.users.currentUser.default_scope
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ const settings = {
|
||||||
hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
|
hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
|
||||||
hideNsfwLocal: user.hideNsfw,
|
hideNsfwLocal: user.hideNsfw,
|
||||||
hideISPLocal: user.hideISP,
|
hideISPLocal: user.hideISP,
|
||||||
|
preloadImage: user.preloadImage,
|
||||||
hidePostStatsLocal: typeof user.hidePostStats === 'undefined'
|
hidePostStatsLocal: typeof user.hidePostStats === 'undefined'
|
||||||
? instance.hidePostStats
|
? instance.hidePostStats
|
||||||
: user.hidePostStats,
|
: user.hidePostStats,
|
||||||
|
@ -46,6 +47,7 @@ const settings = {
|
||||||
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,
|
||||||
|
webPushNotificationsLocal: user.webPushNotifications,
|
||||||
loopSilentAvailable:
|
loopSilentAvailable:
|
||||||
// Firefox
|
// Firefox
|
||||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||||
|
@ -84,6 +86,9 @@ const settings = {
|
||||||
hideNsfwLocal (value) {
|
hideNsfwLocal (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
|
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
|
||||||
},
|
},
|
||||||
|
preloadImage (value) {
|
||||||
|
this.$store.dispatch('setOption', { name: 'preloadImage', value })
|
||||||
|
},
|
||||||
hideISPLocal (value) {
|
hideISPLocal (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'hideISP', value })
|
this.$store.dispatch('setOption', { name: 'hideISP', value })
|
||||||
},
|
},
|
||||||
|
@ -138,6 +143,10 @@ const settings = {
|
||||||
},
|
},
|
||||||
stopGifs (value) {
|
stopGifs (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'stopGifs', value })
|
this.$store.dispatch('setOption', { name: 'stopGifs', value })
|
||||||
|
},
|
||||||
|
webPushNotificationsLocal (value) {
|
||||||
|
this.$store.dispatch('setOption', { name: 'webPushNotifications', value })
|
||||||
|
if (value) this.$store.dispatch('registerPushNotifications')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,6 +118,12 @@
|
||||||
<input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
|
<input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
|
||||||
<label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
|
<label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
|
||||||
</li>
|
</li>
|
||||||
|
<ul class="setting-list suboptions" >
|
||||||
|
<li>
|
||||||
|
<input :disabled="!hideAttachmentsInConvLocal" type="checkbox" id="preloadImage" v-model="preloadImage">
|
||||||
|
<label for="preloadImage">{{$t('settings.preload_images')}}</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<li>
|
<li>
|
||||||
<input type="checkbox" id="stopGifs" v-model="stopGifs">
|
<input type="checkbox" id="stopGifs" v-model="stopGifs">
|
||||||
<label for="stopGifs">{{$t('settings.stop_gifs')}}</label>
|
<label for="stopGifs">{{$t('settings.stop_gifs')}}</label>
|
||||||
|
@ -137,6 +143,18 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<h2>{{$t('settings.notifications')}}</h2>
|
||||||
|
<ul class="setting-list">
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" id="webPushNotifications" v-model="webPushNotificationsLocal">
|
||||||
|
<label for="webPushNotifications">
|
||||||
|
{{$t('settings.enable_web_push_notifications')}}
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :label="$t('settings.theme')" >
|
<div :label="$t('settings.theme')" >
|
||||||
|
|
|
@ -20,7 +20,8 @@ const Status = {
|
||||||
'replies',
|
'replies',
|
||||||
'noReplyLinks',
|
'noReplyLinks',
|
||||||
'noHeading',
|
'noHeading',
|
||||||
'inlineExpanded'
|
'inlineExpanded',
|
||||||
|
'activatePanel'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="status-el" v-if="!hideReply && !deleted" :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 @click.native="activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
|
||||||
<small class="muteWords">{{muteWordHits.join(', ')}}</small>
|
<small class="muteWords">{{muteWordHits.join(', ')}}</small>
|
||||||
<a href="#" class="unmute" @click.prevent="toggleMute"><i class="icon-eye-off"></i></a>
|
<a href="#" class="unmute" @click.prevent="toggleMute"><i class="icon-eye-off"></i></a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,10 +34,10 @@
|
||||||
<h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4>
|
<h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4>
|
||||||
<h4 class="user-name" v-else>{{status.user.name}}</h4>
|
<h4 class="user-name" v-else>{{status.user.name}}</h4>
|
||||||
<span class="links">
|
<span class="links">
|
||||||
<router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link>
|
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link>
|
||||||
<span v-if="status.in_reply_to_screen_name" class="faint reply-info">
|
<span v-if="status.in_reply_to_screen_name" class="faint reply-info">
|
||||||
<i class="icon-right-open"></i>
|
<i class="icon-right-open"></i>
|
||||||
<router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }">
|
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }">
|
||||||
{{status.in_reply_to_screen_name}}
|
{{status.in_reply_to_screen_name}}
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-heading-right">
|
<div class="media-heading-right">
|
||||||
<router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }">
|
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'conversation', params: { id: status.id } }">
|
||||||
<timeago :since="status.created_at" :auto-update="60"></timeago>
|
<timeago :since="status.created_at" :auto-update="60"></timeago>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="visibility-icon" v-if="status.visibility">
|
<div class="visibility-icon" v-if="status.visibility">
|
||||||
|
@ -73,7 +73,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showPreview" class="status-preview-container">
|
<div v-if="showPreview" class="status-preview-container">
|
||||||
<status class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status>
|
<status :activatePanel="activatePanel" class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status>
|
||||||
<div class="status-preview status-preview-loading" v-else>
|
<div class="status-preview status-preview-loading" v-else>
|
||||||
<i class="icon-spin4 animate-spin"></i>
|
<i class="icon-spin4 animate-spin"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,20 +2,20 @@
|
||||||
<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 @click.native="activatePanel('timeline')" to='/user-settings' style="float: right; margin-top:16px;" v-if="!isOtherUser">
|
<router-link @click.native="activatePanel && activatePanel('timeline')" to='/user-settings' style="float: right; margin-top:16px;" v-if="!isOtherUser">
|
||||||
<i class="icon-cog usersettings" :title="$t('tool_tip.user_settings')"></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>
|
||||||
</a>
|
</a>
|
||||||
<div class='container'>
|
<div class='container'>
|
||||||
<router-link :to="{ name: 'user-profile', params: { id: user.id } }">
|
<router-link @click.native="activatePanel && activatePanel('timeline')" :to="{ name: 'user-profile', params: { id: user.id } }">
|
||||||
<StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/>
|
<StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/>
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="name-and-screen-name">
|
<div class="name-and-screen-name">
|
||||||
<div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
|
<div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
|
||||||
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
|
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
|
||||||
<router-link class='user-screen-name':to="{ name: 'user-profile', params: { id: user.id } }">
|
<router-link @click.native="activatePanel && activatePanel('timeline')" class='user-screen-name':to="{ name: 'user-profile', params: { id: user.id } }">
|
||||||
<span>@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
|
<span>@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
|
||||||
<span v-if="!hideUserStatsLocal" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
|
<span v-if="!hideUserStatsLocal" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
|
@ -90,7 +90,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body profile-panel-body">
|
<div class="panel-body profile-panel-body" v-if="switcher">
|
||||||
<div v-if="!hideUserStatsLocal || switcher" class="user-counts" :class="{clickable: switcher}">
|
<div v-if="!hideUserStatsLocal || switcher" class="user-counts" :class="{clickable: switcher}">
|
||||||
<div class="user-count" v-on:click.prevent="setProfileView('statuses')" :class="{selected: selected === 'statuses'}">
|
<div class="user-count" v-on:click.prevent="setProfileView('statuses')" :class="{selected: selected === 'statuses'}">
|
||||||
<h5>{{ $t('user_card.statuses') }}</h5>
|
<h5>{{ $t('user_card.statuses') }}</h5>
|
||||||
|
|
|
@ -125,6 +125,7 @@
|
||||||
"hide_attachments_in_convo": "Hide attachments in conversations",
|
"hide_attachments_in_convo": "Hide attachments in conversations",
|
||||||
"hide_attachments_in_tl": "Hide attachments in timeline",
|
"hide_attachments_in_tl": "Hide attachments in timeline",
|
||||||
"hide_isp": "Hide instance-specific panel",
|
"hide_isp": "Hide instance-specific panel",
|
||||||
|
"preload_images": "Preload images",
|
||||||
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
|
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
|
||||||
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
|
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
|
||||||
"import_followers_from_a_csv_file": "Import follows from a csv file",
|
"import_followers_from_a_csv_file": "Import follows from a csv file",
|
||||||
|
@ -189,6 +190,8 @@
|
||||||
"false": "no",
|
"false": "no",
|
||||||
"true": "yes"
|
"true": "yes"
|
||||||
},
|
},
|
||||||
|
"notifications": "Notifications",
|
||||||
|
"enable_web_push_notifications": "Enable web push notifications",
|
||||||
"style": {
|
"style": {
|
||||||
"switcher": {
|
"switcher": {
|
||||||
"keep_color": "Keep colors",
|
"keep_color": "Keep colors",
|
||||||
|
|
28
src/main.js
28
src/main.js
|
@ -50,6 +50,32 @@ const persistedStateOptions = {
|
||||||
'oauth'
|
'oauth'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const registerPushNotifications = store => {
|
||||||
|
store.subscribe((mutation, state) => {
|
||||||
|
const vapidPublicKey = state.instance.vapidPublicKey
|
||||||
|
const permission = state.interface.notificationPermission === 'granted'
|
||||||
|
const isUserMutation = mutation.type === 'setCurrentUser'
|
||||||
|
|
||||||
|
if (isUserMutation && vapidPublicKey && permission) {
|
||||||
|
return store.dispatch('registerPushNotifications')
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = state.users.currentUser
|
||||||
|
const isVapidMutation = mutation.type === 'setInstanceOption' && mutation.payload.name === 'vapidPublicKey'
|
||||||
|
|
||||||
|
if (isVapidMutation && user && permission) {
|
||||||
|
return store.dispatch('registerPushNotifications')
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPermMutation = mutation.type === 'setNotificationPermission' && mutation.payload === 'granted'
|
||||||
|
|
||||||
|
if (isPermMutation && user && vapidPublicKey) {
|
||||||
|
return store.dispatch('registerPushNotifications')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
createPersistedState(persistedStateOptions).then((persistedState) => {
|
createPersistedState(persistedStateOptions).then((persistedState) => {
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
modules: {
|
modules: {
|
||||||
|
@ -62,7 +88,7 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
|
||||||
chat: chatModule,
|
chat: chatModule,
|
||||||
oauth: oauthModule
|
oauth: oauthModule
|
||||||
},
|
},
|
||||||
plugins: [persistedState],
|
plugins: [persistedState, registerPushNotifications],
|
||||||
strict: false // Socket modifies itself, let's ignore this for now.
|
strict: false // Socket modifies itself, let's ignore this for now.
|
||||||
// strict: process.env.NODE_ENV !== 'production'
|
// strict: process.env.NODE_ENV !== 'production'
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,6 +9,7 @@ const defaultState = {
|
||||||
hideAttachments: false,
|
hideAttachments: false,
|
||||||
hideAttachmentsInConv: false,
|
hideAttachmentsInConv: false,
|
||||||
hideNsfw: true,
|
hideNsfw: true,
|
||||||
|
preloadImage: true,
|
||||||
loopVideo: true,
|
loopVideo: true,
|
||||||
loopVideoSilentOnly: true,
|
loopVideoSilentOnly: true,
|
||||||
autoLoad: true,
|
autoLoad: true,
|
||||||
|
@ -23,6 +24,7 @@ const defaultState = {
|
||||||
likes: true,
|
likes: true,
|
||||||
repeats: true
|
repeats: true
|
||||||
},
|
},
|
||||||
|
webPushNotifications: true,
|
||||||
muteWords: [],
|
muteWords: [],
|
||||||
highlight: {},
|
highlight: {},
|
||||||
interfaceLanguage: browserLocale,
|
interfaceLanguage: browserLocale,
|
||||||
|
|
|
@ -3,7 +3,8 @@ import { set, delete as del } from 'vue'
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
settings: {
|
settings: {
|
||||||
currentSaveStateNotice: null,
|
currentSaveStateNotice: null,
|
||||||
noticeClearTimeout: null
|
noticeClearTimeout: null,
|
||||||
|
notificationPermission: null
|
||||||
},
|
},
|
||||||
browserSupport: {
|
browserSupport: {
|
||||||
cssFilter: window.CSS && window.CSS.supports && (
|
cssFilter: window.CSS && window.CSS.supports && (
|
||||||
|
@ -27,6 +28,9 @@ const interfaceMod = {
|
||||||
} else {
|
} else {
|
||||||
set(state.settings, 'currentSaveStateNotice', { error: true, errorData: error })
|
set(state.settings, 'currentSaveStateNotice', { error: true, errorData: error })
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
setNotificationPermission (state, permission) {
|
||||||
|
state.notificationPermission = permission
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -35,6 +39,9 @@ const interfaceMod = {
|
||||||
},
|
},
|
||||||
settingsSaved ({ commit, dispatch }, { success, error }) {
|
settingsSaved ({ commit, dispatch }, { success, error }) {
|
||||||
commit('settingsSaved', { success, error })
|
commit('settingsSaved', { success, error })
|
||||||
|
},
|
||||||
|
setNotificationPermission ({ commit }, permission) {
|
||||||
|
commit('setNotificationPermission', permission)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import { compact, map, each, merge } from 'lodash'
|
import { compact, map, each, merge } from 'lodash'
|
||||||
import { set } from 'vue'
|
import { set } from 'vue'
|
||||||
|
import registerPushNotifications from '../services/push/push.js'
|
||||||
import oauthApi from '../services/new_api/oauth'
|
import oauthApi from '../services/new_api/oauth'
|
||||||
import { humanizeErrors } from './errors'
|
import { humanizeErrors } from './errors'
|
||||||
|
|
||||||
|
@ -20,6 +21,14 @@ export const mergeOrAdd = (arr, obj, item) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getNotificationPermission = () => {
|
||||||
|
const Notification = window.Notification
|
||||||
|
|
||||||
|
if (!Notification) return Promise.resolve(null)
|
||||||
|
if (Notification.permission === 'default') return Notification.requestPermission()
|
||||||
|
return Promise.resolve(Notification.permission)
|
||||||
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
setMuted (state, { user: { id }, muted }) {
|
setMuted (state, { user: { id }, muted }) {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
|
@ -80,6 +89,13 @@ const users = {
|
||||||
store.rootState.api.backendInteractor.fetchUser({ id })
|
store.rootState.api.backendInteractor.fetchUser({ id })
|
||||||
.then((user) => store.commit('addNewUsers', user))
|
.then((user) => store.commit('addNewUsers', user))
|
||||||
},
|
},
|
||||||
|
registerPushNotifications (store) {
|
||||||
|
const token = store.state.currentUser.credentials
|
||||||
|
const vapidPublicKey = store.rootState.instance.vapidPublicKey
|
||||||
|
const isEnabled = store.rootState.config.webPushNotifications
|
||||||
|
|
||||||
|
registerPushNotifications(isEnabled, vapidPublicKey, token)
|
||||||
|
},
|
||||||
addNewStatuses (store, { statuses }) {
|
addNewStatuses (store, { statuses }) {
|
||||||
const users = map(statuses, 'user')
|
const users = map(statuses, 'user')
|
||||||
const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
|
const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
|
||||||
|
@ -143,6 +159,9 @@ const users = {
|
||||||
commit('setCurrentUser', user)
|
commit('setCurrentUser', user)
|
||||||
commit('addNewUsers', [user])
|
commit('addNewUsers', [user])
|
||||||
|
|
||||||
|
getNotificationPermission()
|
||||||
|
.then(permission => commit('setNotificationPermission', permission))
|
||||||
|
|
||||||
// Set our new backend interactor
|
// Set our new backend interactor
|
||||||
commit('setBackendInteractor', backendInteractorService(accessToken))
|
commit('setBackendInteractor', backendInteractorService(accessToken))
|
||||||
|
|
||||||
|
@ -161,10 +180,6 @@ const users = {
|
||||||
store.commit('addNewUsers', mutedUsers)
|
store.commit('addNewUsers', mutedUsers)
|
||||||
})
|
})
|
||||||
|
|
||||||
if ('Notification' in window && window.Notification.permission === 'default') {
|
|
||||||
window.Notification.requestPermission()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch our friends
|
// Fetch our friends
|
||||||
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
||||||
.then((friends) => commit('addNewUsers', friends))
|
.then((friends) => commit('addNewUsers', friends))
|
||||||
|
|
69
src/services/push/push.js
Normal file
69
src/services/push/push.js
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import runtime from 'serviceworker-webpack-plugin/lib/runtime'
|
||||||
|
|
||||||
|
function urlBase64ToUint8Array (base64String) {
|
||||||
|
const padding = '='.repeat((4 - base64String.length % 4) % 4)
|
||||||
|
const base64 = (base64String + padding)
|
||||||
|
.replace(/-/g, '+')
|
||||||
|
.replace(/_/g, '/')
|
||||||
|
|
||||||
|
const rawData = window.atob(base64)
|
||||||
|
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPushSupported () {
|
||||||
|
return 'serviceWorker' in navigator && 'PushManager' in window
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerServiceWorker () {
|
||||||
|
return runtime.register()
|
||||||
|
.catch((err) => console.error('Unable to register service worker.', err))
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribe (registration, isEnabled, vapidPublicKey) {
|
||||||
|
if (!isEnabled) return Promise.reject(new Error('Web Push is disabled in config'))
|
||||||
|
if (!vapidPublicKey) return Promise.reject(new Error('VAPID public key is not found'))
|
||||||
|
|
||||||
|
const subscribeOptions = {
|
||||||
|
userVisibleOnly: true,
|
||||||
|
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
|
||||||
|
}
|
||||||
|
return registration.pushManager.subscribe(subscribeOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendSubscriptionToBackEnd (subscription, token) {
|
||||||
|
return window.fetch('/api/v1/push/subscription/', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
subscription,
|
||||||
|
data: {
|
||||||
|
alerts: {
|
||||||
|
follow: true,
|
||||||
|
favourite: true,
|
||||||
|
mention: true,
|
||||||
|
reblog: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (!response.ok) throw new Error('Bad status code from server.')
|
||||||
|
return response.json()
|
||||||
|
})
|
||||||
|
.then((responseData) => {
|
||||||
|
if (!responseData.id) throw new Error('Bad response from server.')
|
||||||
|
return responseData
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function registerPushNotifications (isEnabled, vapidPublicKey, token) {
|
||||||
|
if (isPushSupported()) {
|
||||||
|
registerServiceWorker()
|
||||||
|
.then((registration) => subscribe(registration, isEnabled, vapidPublicKey))
|
||||||
|
.then((subscription) => sendSubscriptionToBackEnd(subscription, token))
|
||||||
|
.catch((e) => console.warn(`Failed to setup Web Push Notifications: ${e.message}`))
|
||||||
|
}
|
||||||
|
}
|
38
src/sw.js
Normal file
38
src/sw.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/* eslint-env serviceworker */
|
||||||
|
|
||||||
|
import localForage from 'localforage'
|
||||||
|
|
||||||
|
function isEnabled () {
|
||||||
|
return localForage.getItem('vuex-lz')
|
||||||
|
.then(data => data.config.webPushNotifications)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWindowClients () {
|
||||||
|
return clients.matchAll({ includeUncontrolled: true })
|
||||||
|
.then((clientList) => clientList.filter(({ type }) => type === 'window'))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('push', (event) => {
|
||||||
|
if (event.data) {
|
||||||
|
event.waitUntil(isEnabled().then((isEnabled) => {
|
||||||
|
return isEnabled && getWindowClients().then((list) => {
|
||||||
|
const data = event.data.json()
|
||||||
|
|
||||||
|
if (list.length === 0) return self.registration.showNotification(data.title, data)
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener('notificationclick', (event) => {
|
||||||
|
event.notification.close()
|
||||||
|
|
||||||
|
event.waitUntil(getWindowClients().then((list) => {
|
||||||
|
for (var i = 0; i < list.length; i++) {
|
||||||
|
var client = list[i]
|
||||||
|
if (client.url === '/' && 'focus' in client) { return client.focus() }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clients.openWindow) return clients.openWindow('/')
|
||||||
|
}))
|
||||||
|
})
|
|
@ -3925,7 +3925,7 @@ mime@^1.3.4, mime@^1.5.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||||
|
|
||||||
"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.2:
|
"minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4, minimatch@~3.0.2:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5289,6 +5289,12 @@ serve-static@1.13.1:
|
||||||
parseurl "~1.3.2"
|
parseurl "~1.3.2"
|
||||||
send "0.16.1"
|
send "0.16.1"
|
||||||
|
|
||||||
|
serviceworker-webpack-plugin@0.2.3:
|
||||||
|
version "0.2.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/serviceworker-webpack-plugin/-/serviceworker-webpack-plugin-0.2.3.tgz#1873ed6fc83c873ac8240fac443c615d374feeb2"
|
||||||
|
dependencies:
|
||||||
|
minimatch "^3.0.3"
|
||||||
|
|
||||||
set-blocking@^2.0.0, set-blocking@~2.0.0:
|
set-blocking@^2.0.0, set-blocking@~2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||||
|
|
Loading…
Add table
Reference in a new issue