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 utils = require('./utils')
|
||||
var projectRoot = path.resolve(__dirname, '../')
|
||||
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
|
||||
|
||||
var env = process.env.NODE_ENV
|
||||
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
|
||||
|
@ -91,5 +92,10 @@ module.exports = {
|
|||
browsers: ['last 2 versions']
|
||||
})
|
||||
]
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
new ServiceWorkerWebpackPlugin({
|
||||
entry: path.join(__dirname, '..', 'src/sw.js')
|
||||
})
|
||||
]
|
||||
}
|
||||
|
|
|
@ -90,6 +90,7 @@
|
|||
"raw-loader": "^0.5.1",
|
||||
"selenium-server": "2.53.1",
|
||||
"semver": "^5.3.0",
|
||||
"serviceworker-webpack-plugin": "0.2.3",
|
||||
"shelljs": "^0.7.4",
|
||||
"sinon": "^1.17.3",
|
||||
"sinon-chai": "^2.8.0",
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
|
||||
<features-panel v-if="!currentUser"></features-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>
|
||||
|
|
|
@ -17,17 +17,21 @@ import FollowRequests from '../components/follow_requests/follow_requests.vue'
|
|||
import OAuthCallback from '../components/oauth_callback/oauth_callback.vue'
|
||||
import UserSearch from '../components/user_search/user_search.vue'
|
||||
|
||||
const afterStoreSetup = ({store, i18n}) => {
|
||||
const afterStoreSetup = ({ store, i18n }) => {
|
||||
window.fetch('/api/statusnet/config.json')
|
||||
.then((res) => res.json())
|
||||
.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: 'registrationOpen', value: (registrationClosed === '0') })
|
||||
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
|
||||
store.dispatch('setInstanceOption', { name: 'server', value: server })
|
||||
|
||||
if (vapidPublicKey) {
|
||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||
}
|
||||
|
||||
var apiConfig = data.site.pleromafe
|
||||
|
||||
window.fetch('/static/config.json')
|
||||
|
|
|
@ -13,6 +13,7 @@ const Attachment = {
|
|||
return {
|
||||
nsfwImage,
|
||||
hideNsfwLocal: this.$store.state.config.hideNsfw,
|
||||
preloadImage: this.$store.state.config.preloadImage,
|
||||
loopVideo: this.$store.state.config.loopVideo,
|
||||
showHidden: false,
|
||||
loading: false,
|
||||
|
@ -46,7 +47,7 @@ const Attachment = {
|
|||
}
|
||||
},
|
||||
toggleHidden () {
|
||||
if (this.img) {
|
||||
if (this.img && !this.preloadImage) {
|
||||
if (this.img.onload) {
|
||||
this.img.onload()
|
||||
} else {
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
<div class="hider" v-if="nsfw && hideNsfwLocal && !hidden">
|
||||
<a href="#" @click.prevent="toggleHidden()">Hide</a>
|
||||
</div>
|
||||
|
||||
<a v-if="type === 'image' && !hidden" class="image-attachment" :href="attachment.url" target="_blank" :title="attachment.description">
|
||||
<a v-if="type === 'image' && (!hidden || preloadImage)" class="image-attachment" :class="{'hidden': hidden && preloadImage}" :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"/>
|
||||
</a>
|
||||
|
||||
|
@ -161,6 +160,10 @@
|
|||
display: flex;
|
||||
flex: 1;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.still-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
|
@ -11,7 +11,8 @@ const Notification = {
|
|||
}
|
||||
},
|
||||
props: [
|
||||
'notification'
|
||||
'notification',
|
||||
'activatePanel'
|
||||
],
|
||||
components: {
|
||||
Status, StillImage, UserCardContent
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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>
|
||||
<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"/>
|
||||
|
@ -25,13 +25,13 @@
|
|||
<small>{{$t('notifications.followed_you')}}</small>
|
||||
</span>
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
{{$t('notifications.broken_favorite')}}
|
||||
</div>
|
||||
|
|
|
@ -4,6 +4,7 @@ import notificationsFetcher from '../../services/notifications_fetcher/notificat
|
|||
import { sortBy, filter } from 'lodash'
|
||||
|
||||
const Notifications = {
|
||||
props: [ 'activatePanel' ],
|
||||
created () {
|
||||
const store = this.$store
|
||||
const credentials = store.state.users.currentUser.credentials
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
<div class="panel-body">
|
||||
<div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'>
|
||||
<div class="notification-overlay"></div>
|
||||
<notification :notification="notification"></notification>
|
||||
<notification :activatePanel="activatePanel" :notification="notification"></notification>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
|
|
|
@ -46,7 +46,7 @@ const PostStatusForm = {
|
|||
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.$store.state.users.currentUser.default_scope
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ const settings = {
|
|||
hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
|
||||
hideNsfwLocal: user.hideNsfw,
|
||||
hideISPLocal: user.hideISP,
|
||||
preloadImage: user.preloadImage,
|
||||
hidePostStatsLocal: typeof user.hidePostStats === 'undefined'
|
||||
? instance.hidePostStats
|
||||
: user.hidePostStats,
|
||||
|
@ -46,6 +47,7 @@ const settings = {
|
|||
scopeCopyLocal: user.scopeCopy,
|
||||
scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy),
|
||||
stopGifs: user.stopGifs,
|
||||
webPushNotificationsLocal: user.webPushNotifications,
|
||||
loopSilentAvailable:
|
||||
// Firefox
|
||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||
|
@ -84,6 +86,9 @@ const settings = {
|
|||
hideNsfwLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
|
||||
},
|
||||
preloadImage (value) {
|
||||
this.$store.dispatch('setOption', { name: 'preloadImage', value })
|
||||
},
|
||||
hideISPLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'hideISP', value })
|
||||
},
|
||||
|
@ -138,6 +143,10 @@ const settings = {
|
|||
},
|
||||
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">
|
||||
<label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
|
||||
</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>
|
||||
<input type="checkbox" id="stopGifs" v-model="stopGifs">
|
||||
<label for="stopGifs">{{$t('settings.stop_gifs')}}</label>
|
||||
|
@ -137,6 +143,18 @@
|
|||
</li>
|
||||
</ul>
|
||||
</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 :label="$t('settings.theme')" >
|
||||
|
|
|
@ -20,7 +20,8 @@ const Status = {
|
|||
'replies',
|
||||
'noReplyLinks',
|
||||
'noHeading',
|
||||
'inlineExpanded'
|
||||
'inlineExpanded',
|
||||
'activatePanel'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="status-el" v-if="!hideReply && !deleted" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
|
||||
<template v-if="muted && !noReplyLinks">
|
||||
<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>
|
||||
<a href="#" class="unmute" @click.prevent="toggleMute"><i class="icon-eye-off"></i></a>
|
||||
</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-else>{{status.user.name}}</h4>
|
||||
<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">
|
||||
<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}}
|
||||
</router-link>
|
||||
</span>
|
||||
|
@ -54,7 +54,7 @@
|
|||
</h4>
|
||||
</div>
|
||||
<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>
|
||||
</router-link>
|
||||
<div class="visibility-icon" v-if="status.visibility">
|
||||
|
@ -73,7 +73,7 @@
|
|||
</div>
|
||||
|
||||
<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>
|
||||
<i class="icon-spin4 animate-spin"></i>
|
||||
</div>
|
||||
|
|
|
@ -2,20 +2,20 @@
|
|||
<div id="heading" class="profile-panel-background" :style="headingStyle">
|
||||
<div class="panel-heading text-center">
|
||||
<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>
|
||||
</router-link>
|
||||
<a :href="user.statusnet_profile_url" target="_blank" class="floater" v-if="isOtherUser">
|
||||
<i class="icon-link-ext usersettings"></i>
|
||||
</a>
|
||||
<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"/>
|
||||
</router-link>
|
||||
<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-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 v-if="!hideUserStatsLocal" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
|
||||
</router-link>
|
||||
|
@ -90,7 +90,7 @@
|
|||
</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 class="user-count" v-on:click.prevent="setProfileView('statuses')" :class="{selected: selected === 'statuses'}">
|
||||
<h5>{{ $t('user_card.statuses') }}</h5>
|
||||
|
|
|
@ -125,6 +125,7 @@
|
|||
"hide_attachments_in_convo": "Hide attachments in conversations",
|
||||
"hide_attachments_in_tl": "Hide attachments in timeline",
|
||||
"hide_isp": "Hide instance-specific panel",
|
||||
"preload_images": "Preload images",
|
||||
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
|
||||
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
|
||||
"import_followers_from_a_csv_file": "Import follows from a csv file",
|
||||
|
@ -132,7 +133,7 @@
|
|||
"inputRadius": "Input fields",
|
||||
"checkboxRadius": "Checkboxes",
|
||||
"instance_default": "(default: {value})",
|
||||
"instance_default_simple" : "(default)",
|
||||
"instance_default_simple": "(default)",
|
||||
"interface": "Interface",
|
||||
"interfaceLanguage": "Interface language",
|
||||
"invalid_theme_imported": "The selected file is not a supported Pleroma theme. No changes to your theme were made.",
|
||||
|
@ -189,6 +190,8 @@
|
|||
"false": "no",
|
||||
"true": "yes"
|
||||
},
|
||||
"notifications": "Notifications",
|
||||
"enable_web_push_notifications": "Enable web push notifications",
|
||||
"style": {
|
||||
"switcher": {
|
||||
"keep_color": "Keep colors",
|
||||
|
|
30
src/main.js
30
src/main.js
|
@ -50,6 +50,32 @@ const persistedStateOptions = {
|
|||
'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) => {
|
||||
const store = new Vuex.Store({
|
||||
modules: {
|
||||
|
@ -62,12 +88,12 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
|
|||
chat: chatModule,
|
||||
oauth: oauthModule
|
||||
},
|
||||
plugins: [persistedState],
|
||||
plugins: [persistedState, registerPushNotifications],
|
||||
strict: false // Socket modifies itself, let's ignore this for now.
|
||||
// strict: process.env.NODE_ENV !== 'production'
|
||||
})
|
||||
|
||||
afterStoreSetup({store, i18n})
|
||||
afterStoreSetup({ store, i18n })
|
||||
})
|
||||
|
||||
// These are inlined by webpack's DefinePlugin
|
||||
|
|
|
@ -9,6 +9,7 @@ const defaultState = {
|
|||
hideAttachments: false,
|
||||
hideAttachmentsInConv: false,
|
||||
hideNsfw: true,
|
||||
preloadImage: true,
|
||||
loopVideo: true,
|
||||
loopVideoSilentOnly: true,
|
||||
autoLoad: true,
|
||||
|
@ -23,6 +24,7 @@ const defaultState = {
|
|||
likes: true,
|
||||
repeats: true
|
||||
},
|
||||
webPushNotifications: true,
|
||||
muteWords: [],
|
||||
highlight: {},
|
||||
interfaceLanguage: browserLocale,
|
||||
|
|
|
@ -3,12 +3,13 @@ import { set, delete as del } from 'vue'
|
|||
const defaultState = {
|
||||
settings: {
|
||||
currentSaveStateNotice: null,
|
||||
noticeClearTimeout: null
|
||||
noticeClearTimeout: null,
|
||||
notificationPermission: null
|
||||
},
|
||||
browserSupport: {
|
||||
cssFilter: window.CSS && window.CSS.supports && (
|
||||
window.CSS.supports('filter', 'drop-shadow(0 0)') ||
|
||||
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
|
||||
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -23,10 +24,13 @@ const interfaceMod = {
|
|||
}
|
||||
set(state.settings, 'currentSaveStateNotice', { error: false, data: success })
|
||||
set(state.settings, 'noticeClearTimeout',
|
||||
setTimeout(() => del(state.settings, 'currentSaveStateNotice'), 2000))
|
||||
setTimeout(() => del(state.settings, 'currentSaveStateNotice'), 2000))
|
||||
} else {
|
||||
set(state.settings, 'currentSaveStateNotice', { error: true, errorData: error })
|
||||
}
|
||||
},
|
||||
setNotificationPermission (state, permission) {
|
||||
state.notificationPermission = permission
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
@ -35,6 +39,9 @@ const interfaceMod = {
|
|||
},
|
||||
settingsSaved ({ commit, dispatch }, { success, error }) {
|
||||
commit('settingsSaved', { success, error })
|
||||
},
|
||||
setNotificationPermission ({ commit }, permission) {
|
||||
commit('setNotificationPermission', permission)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import { compact, map, each, merge } from 'lodash'
|
||||
import { set } from 'vue'
|
||||
import registerPushNotifications from '../services/push/push.js'
|
||||
import oauthApi from '../services/new_api/oauth'
|
||||
import {humanizeErrors} from './errors'
|
||||
import { humanizeErrors } from './errors'
|
||||
|
||||
// TODO: Unify with mergeOrAdd in statuses.js
|
||||
export const mergeOrAdd = (arr, obj, item) => {
|
||||
|
@ -11,17 +12,25 @@ export const mergeOrAdd = (arr, obj, item) => {
|
|||
if (oldItem) {
|
||||
// We already have this, so only merge the new info.
|
||||
merge(oldItem, item)
|
||||
return {item: oldItem, new: false}
|
||||
return { item: oldItem, new: false }
|
||||
} else {
|
||||
// This is a new item, prepare it
|
||||
arr.push(item)
|
||||
obj[item.id] = item
|
||||
return {item, new: true}
|
||||
return { item, new: true }
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
setMuted (state, { user: {id}, muted }) {
|
||||
setMuted (state, { user: { id }, muted }) {
|
||||
const user = state.usersObject[id]
|
||||
set(user, 'muted', muted)
|
||||
},
|
||||
|
@ -45,7 +54,7 @@ export const mutations = {
|
|||
setUserForStatus (state, status) {
|
||||
status.user = state.usersObject[status.user.id]
|
||||
},
|
||||
setColor (state, { user: {id}, highlighted }) {
|
||||
setColor (state, { user: { id }, highlighted }) {
|
||||
const user = state.usersObject[id]
|
||||
set(user, 'highlight', highlighted)
|
||||
},
|
||||
|
@ -77,9 +86,16 @@ const users = {
|
|||
mutations,
|
||||
actions: {
|
||||
fetchUser (store, id) {
|
||||
store.rootState.api.backendInteractor.fetchUser({id})
|
||||
store.rootState.api.backendInteractor.fetchUser({ id })
|
||||
.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 }) {
|
||||
const users = map(statuses, 'user')
|
||||
const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
|
||||
|
@ -143,6 +159,9 @@ const users = {
|
|||
commit('setCurrentUser', user)
|
||||
commit('addNewUsers', [user])
|
||||
|
||||
getNotificationPermission()
|
||||
.then(permission => commit('setNotificationPermission', permission))
|
||||
|
||||
// Set our new backend interactor
|
||||
commit('setBackendInteractor', backendInteractorService(accessToken))
|
||||
|
||||
|
@ -161,12 +180,8 @@ 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({id: user.id})
|
||||
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
||||
.then((friends) => commit('addNewUsers', friends))
|
||||
})
|
||||
} else {
|
||||
|
|
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
|
||||
dependencies:
|
||||
|
@ -5289,6 +5289,12 @@ serve-static@1.13.1:
|
|||
parseurl "~1.3.2"
|
||||
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:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
|
|
Loading…
Add table
Reference in a new issue