Removed monetizejs sponsorship support.

Reduced time counter increment interval.
Added badge service.
Refactored user service.
Replaced Google+ with People API.
This commit is contained in:
Benoit Schweblin 2019-06-22 22:19:01 +01:00
parent 2a865ddb44
commit 1b2d48ff22
68 changed files with 1082 additions and 350 deletions

View File

@ -41,12 +41,9 @@ exports.generate = (req, res) => {
const outputFormat = Object.prototype.hasOwnProperty.call(outputFormats, req.query.format) const outputFormat = Object.prototype.hasOwnProperty.call(outputFormats, req.query.format)
? req.query.format ? req.query.format
: 'pdf'; : 'pdf';
Promise.all([ user.checkSponsor(req.query.idToken)
user.checkSponsor(req.query.idToken), .then((isSponsor) => {
user.checkMonetize(req.query.token), if (!isSponsor) {
])
.then(([isSponsor, isMonetize]) => {
if (!isSponsor && !isMonetize) {
throw new Error('unauthorized'); throw new Error('unauthorized');
} }
@ -79,7 +76,7 @@ exports.generate = (req, res) => {
if (!Number.isNaN(options.tocDepth)) { if (!Number.isNaN(options.tocDepth)) {
params.push('--toc-depth', options.tocDepth); params.push('--toc-depth', options.tocDepth);
} }
options.highlightStyle = highlightStyles.indexOf(options.highlightStyle) !== -1 ? options.highlightStyle : 'kate'; options.highlightStyle = highlightStyles.includes(options.highlightStyle) ? options.highlightStyle : 'kate';
params.push('--highlight-style', options.highlightStyle); params.push('--highlight-style', options.highlightStyle);
Object.keys(metadata).forEach((key) => { Object.keys(metadata).forEach((key) => {
params.push('-M', `${key}=${metadata[key]}`); params.push('-M', `${key}=${metadata[key]}`);

View File

@ -50,12 +50,9 @@ const readJson = (str) => {
exports.generate = (req, res) => { exports.generate = (req, res) => {
let wkhtmltopdfError = ''; let wkhtmltopdfError = '';
Promise.all([ user.checkSponsor(req.query.idToken)
user.checkSponsor(req.query.idToken), .then((isSponsor) => {
user.checkMonetize(req.query.token), if (!isSponsor) {
])
.then(([isSponsor, isMonetize]) => {
if (!isSponsor && !isMonetize) {
throw new Error('unauthorized'); throw new Error('unauthorized');
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -127,7 +124,7 @@ exports.generate = (req, res) => {
} }
// Page size // Page size
params.push('--page-size', authorizedPageSizes.indexOf(options.pageSize) === -1 ? 'A4' : options.pageSize); params.push('--page-size', !authorizedPageSizes.includes(options.pageSize) ? 'A4' : options.pageSize);
// Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason // Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason
const binPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf'; const binPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';

View File

@ -116,21 +116,3 @@ exports.checkSponsor = (idToken) => {
return exports.getUserFromToken(idToken) return exports.getUserFromToken(idToken)
.then(userInfo => userInfo && userInfo.sponsorUntil > Date.now(), () => false); .then(userInfo => userInfo && userInfo.sponsorUntil > Date.now(), () => false);
}; };
exports.checkMonetize = (token) => {
if (!token) {
return Promise.resolve(false);
}
return new Promise(resolve => request({
uri: 'https://monetizejs.com/api/payments',
qs: {
access_token: token,
},
json: true,
}, (err, paymentsRes, payments) => {
const authorized = payments && payments.app === 'ESTHdCYOi18iLhhO' && (
(payments.chargeOption && payments.chargeOption.alias === 'once') ||
(payments.subscriptionOption && payments.subscriptionOption.alias === 'yearly'));
resolve(!err && paymentsRes.statusCode === 200 && authorized);
}));
};

View File

@ -19,7 +19,6 @@ import ContextMenu from './ContextMenu';
import SplashScreen from './SplashScreen'; import SplashScreen from './SplashScreen';
import syncSvc from '../services/syncSvc'; import syncSvc from '../services/syncSvc';
import networkSvc from '../services/networkSvc'; import networkSvc from '../services/networkSvc';
import sponsorSvc from '../services/sponsorSvc';
import tempFileSvc from '../services/tempFileSvc'; import tempFileSvc from '../services/tempFileSvc';
import store from '../store'; import store from '../store';
import './common/vueGlobals'; import './common/vueGlobals';
@ -55,7 +54,6 @@ export default {
try { try {
await syncSvc.init(); await syncSvc.init();
await networkSvc.init(); await networkSvc.init();
await sponsorSvc.init();
this.ready = true; this.ready = true;
tempFileSvc.setReady(); tempFileSvc.setReady();
} catch (err) { } catch (err) {

View File

@ -22,6 +22,7 @@ import { mapMutations, mapActions } from 'vuex';
import workspaceSvc from '../services/workspaceSvc'; import workspaceSvc from '../services/workspaceSvc';
import explorerSvc from '../services/explorerSvc'; import explorerSvc from '../services/explorerSvc';
import store from '../store'; import store from '../store';
import badgeSvc from '../services/badgeSvc';
export default { export default {
name: 'explorer-node', // Required for recursivity name: 'explorer-node', // Required for recursivity
@ -81,7 +82,7 @@ export default {
]), ]),
select(id = this.node.item.id, doOpen = true) { select(id = this.node.item.id, doOpen = true) {
const node = store.getters['explorer/nodeMap'][id]; const node = store.getters['explorer/nodeMap'][id];
if (!node) { if (!node || node.item.id === store.state.explorer.selectedId) {
return false; return false;
} }
store.commit('explorer/setSelectedId', id); store.commit('explorer/setSelectedId', id);
@ -92,6 +93,7 @@ export default {
store.commit('explorer/toggleOpenNode', id); store.commit('explorer/toggleOpenNode', id);
} else { } else {
store.commit('file/setCurrentId', id); store.commit('file/setCurrentId', id);
badgeSvc.addBadge('switchFile');
} }
}, 10); }, 10);
} }
@ -104,9 +106,11 @@ export default {
if (newChildNode.isFolder) { if (newChildNode.isFolder) {
const item = await workspaceSvc.storeItem(newChildNode.item); const item = await workspaceSvc.storeItem(newChildNode.item);
this.select(item.id); this.select(item.id);
badgeSvc.addBadge('createFolder');
} else { } else {
const item = await workspaceSvc.createFile(newChildNode.item); const item = await workspaceSvc.createFile(newChildNode.item);
this.select(item.id); this.select(item.id);
badgeSvc.addBadge('createFile');
} }
} catch (e) { } catch (e) {
// Cancel // Cancel
@ -115,15 +119,16 @@ export default {
store.commit('explorer/setNewItem', null); store.commit('explorer/setNewItem', null);
}, },
async submitEdit(cancel) { async submitEdit(cancel) {
const { item } = store.getters['explorer/editingNode']; const { item, isFolder } = store.getters['explorer/editingNode'];
const value = this.editingValue; const value = this.editingValue;
this.setEditingId(null); this.setEditingId(null);
if (!cancel && item.id && value) { if (!cancel && item.id && value && item.name !== value) {
try { try {
await workspaceSvc.storeItem({ await workspaceSvc.storeItem({
...item, ...item,
name: value, name: value,
}); });
badgeSvc.addBadge(isFolder ? 'renameFolder' : 'renameFile');
} catch (e) { } catch (e) {
// Cancel // Cancel
} }
@ -151,6 +156,7 @@ export default {
...sourceNode.item, ...sourceNode.item,
parentId: targetNode.item.id, parentId: targetNode.item.id,
}); });
badgeSvc.addBadge('moveFiles');
} }
}, },
async onContextMenu(evt) { async onContextMenu(evt) {

View File

@ -177,11 +177,9 @@ export default {
.sticky-comment, .sticky-comment,
.current-discussion { .current-discussion {
background-color: mix(#000, $editor-background-light, 6.7%); background-color: mix(#000, $editor-background-light, 6.7%);
border-color: $editor-background-light;
.app--dark & { .app--dark & {
background-color: mix(#fff, $editor-background-dark, 6.7%); background-color: mix(#fff, $editor-background-dark, 6.7%);
border-color: $editor-background-dark;
} }
} }
} }
@ -204,7 +202,6 @@ $preview-background-dark: #252525;
.sticky-comment, .sticky-comment,
.current-discussion { .current-discussion {
background-color: mix(#000, $preview-background-light, 6.7%); background-color: mix(#000, $preview-background-light, 6.7%);
border-color: $preview-background-light;
} }
} }

View File

@ -37,6 +37,7 @@ import SyncManagementModal from './modals/SyncManagementModal';
import PublishManagementModal from './modals/PublishManagementModal'; import PublishManagementModal from './modals/PublishManagementModal';
import WorkspaceManagementModal from './modals/WorkspaceManagementModal'; import WorkspaceManagementModal from './modals/WorkspaceManagementModal';
import AccountManagementModal from './modals/AccountManagementModal'; import AccountManagementModal from './modals/AccountManagementModal';
import BadgeManagementModal from './modals/BadgeManagementModal';
import SponsorModal from './modals/SponsorModal'; import SponsorModal from './modals/SponsorModal';
// Providers // Providers
@ -88,6 +89,7 @@ export default {
PublishManagementModal, PublishManagementModal,
WorkspaceManagementModal, WorkspaceManagementModal,
AccountManagementModal, AccountManagementModal,
BadgeManagementModal,
SponsorModal, SponsorModal,
// Providers // Providers
GooglePhotoModal, GooglePhotoModal,

View File

@ -57,6 +57,7 @@ import utils from '../services/utils';
import pagedownButtons from '../data/pagedownButtons'; import pagedownButtons from '../data/pagedownButtons';
import store from '../store'; import store from '../store';
import workspaceSvc from '../services/workspaceSvc'; import workspaceSvc from '../services/workspaceSvc';
import badgeSvc from '../services/badgeSvc';
// According to mousetrap // According to mousetrap
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl'; const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
@ -178,7 +179,7 @@ export default {
}, },
requestSync() { requestSync() {
if (this.isSyncPossible && !this.isSyncRequested) { if (this.isSyncPossible && !this.isSyncRequested) {
syncSvc.requestSync(); syncSvc.requestSync(true);
} }
}, },
requestPublish() { requestPublish() {
@ -188,7 +189,11 @@ export default {
}, },
pagedownClick(name) { pagedownClick(name) {
if (store.getters['content/isCurrentEditable']) { if (store.getters['content/isCurrentEditable']) {
const text = editorSvc.clEditor.getContent();
editorSvc.pagedownEditor.uiManager.doClick(name); editorSvc.pagedownEditor.uiManager.doClick(name);
if (text !== editorSvc.clEditor.getContent()) {
badgeSvc.addBadge('formatButtons');
}
} }
}, },
async editTitle(toggle) { async editTitle(toggle) {
@ -198,12 +203,13 @@ export default {
} else { } else {
const title = this.title.trim(); const title = this.title.trim();
this.title = store.getters['file/current'].name; this.title = store.getters['file/current'].name;
if (title) { if (title && this.title !== title) {
try { try {
await workspaceSvc.storeItem({ await workspaceSvc.storeItem({
...store.getters['file/current'], ...store.getters['file/current'],
name: title, name: title,
}); });
badgeSvc.addBadge('editCurrentFileName');
} catch (e) { } catch (e) {
// Cancel // Cancel
} }

View File

@ -3,6 +3,7 @@
<div class="notification__item flex flex--row flex--align-center" v-for="(item, idx) in items" :key="idx"> <div class="notification__item flex flex--row flex--align-center" v-for="(item, idx) in items" :key="idx">
<div class="notification__icon flex flex--column flex--center"> <div class="notification__icon flex flex--column flex--center">
<icon-alert v-if="item.type === 'error'"></icon-alert> <icon-alert v-if="item.type === 'error'"></icon-alert>
<icon-check-circle v-if="item.type === 'badge'"></icon-check-circle>
<icon-information v-else></icon-information> <icon-information v-else></icon-information>
</div> </div>
<div class="notification__content"> <div class="notification__content">

View File

@ -19,7 +19,7 @@
<history-menu v-else-if="panel === 'history'"></history-menu> <history-menu v-else-if="panel === 'history'"></history-menu>
<export-menu v-else-if="panel === 'export'"></export-menu> <export-menu v-else-if="panel === 'export'"></export-menu>
<import-export-menu v-else-if="panel === 'importExport'"></import-export-menu> <import-export-menu v-else-if="panel === 'importExport'"></import-export-menu>
<workspace-backup-menu v-else-if="panel === 'workspaceBackup'"></workspace-backup-menu> <workspace-backup-menu v-else-if="panel === 'workspaceBackups'"></workspace-backup-menu>
<div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help"> <div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help">
<pre class="markdown-highlighting" v-html="markdownSample"></pre> <pre class="markdown-highlighting" v-html="markdownSample"></pre>
</div> </div>
@ -54,7 +54,7 @@ const panelNames = {
publish: 'Publish', publish: 'Publish',
history: 'File history', history: 'File history',
importExport: 'Import/export', importExport: 'Import/export',
workspaceBackup: 'Workspace backup', workspaceBackups: 'Workspace backups',
}; };
export default { export default {
@ -174,8 +174,9 @@ export default {
font-size: 0.95em; font-size: 0.95em;
p { p {
margin: 10px; margin: 10px 15px;
line-height: 1.5; line-height: 1.5;
font-style: italic;
} }
} }
</style> </style>

View File

@ -10,14 +10,17 @@ import store from '../store';
export default { export default {
props: ['userId'], props: ['userId'],
computed: { computed: {
sanitizedUserId() {
return userSvc.sanitizeUserId(this.userId);
},
url() { url() {
const userInfo = store.state.userInfo.itemsById[this.userId]; const userInfo = store.state.userInfo.itemsById[this.sanitizedUserId];
return userInfo && userInfo.imageUrl && `url('${userInfo.imageUrl}')`; return userInfo && userInfo.imageUrl && `url('${userInfo.imageUrl}')`;
}, },
}, },
watch: { watch: {
userId: { sanitizedUserId: {
handler: userId => userSvc.getInfo(userId), handler: sanitizedUserId => userSvc.addUserId(sanitizedUserId),
immediate: true, immediate: true,
}, },
}, },

View File

@ -9,14 +9,17 @@ import store from '../store';
export default { export default {
props: ['userId'], props: ['userId'],
computed: { computed: {
sanitizedUserId() {
return userSvc.sanitizeUserId(this.userId);
},
name() { name() {
const userInfo = store.state.userInfo.itemsById[this.userId]; const userInfo = store.state.userInfo.itemsById[this.sanitizedUserId];
return userInfo ? userInfo.name : 'Someone'; return userInfo ? userInfo.name : 'Someone';
}, },
}, },
watch: { watch: {
userId: { sanitizedUserId: {
handler: userId => userSvc.getInfo(userId), handler: sanitizedUserId => userSvc.addUserId(sanitizedUserId),
immediate: true, immediate: true,
}, },
}, },

View File

@ -75,6 +75,6 @@ Vue.directive('clipboard', {
// Global filters // Global filters
Vue.filter('formatTime', time => Vue.filter('formatTime', time =>
// Access the minute counter for reactive refresh // Access the time counter for reactive refresh
timeSvc.format(time, store.state.minuteCounter)); timeSvc.format(time, store.state.timeCounter));

View File

@ -28,6 +28,7 @@ import UserName from '../UserName';
import editorSvc from '../../services/editorSvc'; import editorSvc from '../../services/editorSvc';
import htmlSanitizer from '../../libs/htmlSanitizer'; import htmlSanitizer from '../../libs/htmlSanitizer';
import store from '../../store'; import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
export default { export default {
components: { components: {
@ -52,6 +53,7 @@ export default {
try { try {
await store.dispatch('modal/open', 'commentDeletion'); await store.dispatch('modal/open', 'commentDeletion');
store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment }); store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment });
badgeSvc.addBadge('removeComment');
} catch (e) { } catch (e) {
// Cancel // Cancel
} }

View File

@ -133,11 +133,6 @@ export default {
: editorSvc.previewElt.parentNode; : editorSvc.previewElt.parentNode;
this.updateSticky = () => { this.updateSticky = () => {
const commitIfDifferent = (value) => {
if (store.state.discussion.stickyComment !== value) {
store.commit('discussion/setStickyComment', value);
}
};
let height = 0; let height = 0;
let offsetTop = this.tops.current; let offsetTop = this.tops.current;
const lastCommentElt = this.$el.querySelector(`.comment--${this.currentDiscussionLastCommentId}`); const lastCommentElt = this.$el.querySelector(`.comment--${this.currentDiscussionLastCommentId}`);
@ -152,13 +147,15 @@ export default {
const currentDiscussionElt = document.querySelector('.current-discussion__inner'); const currentDiscussionElt = document.querySelector('.current-discussion__inner');
const minOffsetTop = this.scrollerElt.scrollTop + 10; const minOffsetTop = this.scrollerElt.scrollTop + 10;
const maxOffsetTop = (this.scrollerElt.scrollTop + this.scrollerElt.clientHeight) - height const maxOffsetTop = (this.scrollerElt.scrollTop + this.scrollerElt.clientHeight) - height
- currentDiscussionElt.clientHeight - 10; - currentDiscussionElt.clientHeight;
let stickyComment = null;
if (offsetTop > maxOffsetTop || maxOffsetTop < minOffsetTop) { if (offsetTop > maxOffsetTop || maxOffsetTop < minOffsetTop) {
commitIfDifferent('bottom'); stickyComment = 'bottom';
} else if (offsetTop < minOffsetTop) { } else if (offsetTop < minOffsetTop) {
commitIfDifferent('top'); stickyComment = 'top';
} else { }
commitIfDifferent(null); if (store.state.discussion.stickyComment !== stickyComment) {
store.commit('discussion/setStickyComment', stickyComment);
} }
}; };
@ -199,19 +196,6 @@ export default {
padding-top: 10px; padding-top: 10px;
} }
.comment-list__current-discussion {
border-top: 2px solid;
border-bottom: 2px solid;
.comment-list--top & {
border-bottom-color: transparent;
}
.comment-list--bottom & {
border-top-color: transparent;
}
}
/* use div selector to avoid collision with Prism */ /* use div selector to avoid collision with Prism */
div.comment { div.comment {
padding: 5px 10px 10px; padding: 5px 10px 10px;

View File

@ -34,6 +34,7 @@ import animationSvc from '../../services/animationSvc';
import markdownConversionSvc from '../../services/markdownConversionSvc'; import markdownConversionSvc from '../../services/markdownConversionSvc';
import StickyComment from './StickyComment'; import StickyComment from './StickyComment';
import store from '../../store'; import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
export default { export default {
components: { components: {
@ -103,6 +104,7 @@ export default {
store.dispatch('discussion/cleanCurrentFile', { store.dispatch('discussion/cleanCurrentFile', {
filterDiscussion: this.currentDiscussion, filterDiscussion: this.currentDiscussion,
}); });
badgeSvc.addBadge('removeDiscussion');
} catch (e) { } catch (e) {
// Cancel // Cancel
} }
@ -118,7 +120,6 @@ export default {
position: absolute; position: absolute;
right: 0; right: 0;
bottom: 0; bottom: 0;
border-top: 2px solid;
.sticky-comment { .sticky-comment {
position: relative; position: relative;

View File

@ -30,6 +30,7 @@ import markdownConversionSvc from '../../services/markdownConversionSvc';
import utils from '../../services/utils'; import utils from '../../services/utils';
import userSvc from '../../services/userSvc'; import userSvc from '../../services/userSvc';
import store from '../../store'; import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
export default { export default {
components: { components: {
@ -70,12 +71,15 @@ export default {
[utils.uid()]: comment, [utils.uid()]: comment,
}, },
}; };
// Create discussion
if (discussionId === store.state.discussion.newDiscussionId) { if (discussionId === store.state.discussion.newDiscussionId) {
// Create discussion
patch.discussions = { patch.discussions = {
...store.getters['content/current'].discussions, ...store.getters['content/current'].discussions,
[discussionId]: store.getters['discussion/newDiscussion'], [discussionId]: store.getters['discussion/newDiscussion'],
}; };
badgeSvc.addBadge('createDiscussion');
} else {
badgeSvc.addBadge('addComment');
} }
store.dispatch('content/patchCurrent', patch); store.dispatch('content/patchCurrent', patch);
store.commit('discussion/setNewCommentText'); store.commit('discussion/setNewCommentText');

View File

@ -40,7 +40,6 @@ export default {
right: 0; right: 0;
font-size: 15px; font-size: 15px;
padding-top: 10px; padding-top: 10px;
border-bottom: 2px solid;
.current-discussion & { .current-discussion & {
width: auto !important; width: auto !important;

View File

@ -56,6 +56,7 @@ import utils from '../../services/utils';
import googleHelper from '../../services/providers/helpers/googleHelper'; import googleHelper from '../../services/providers/helpers/googleHelper';
import syncSvc from '../../services/syncSvc'; import syncSvc from '../../services/syncSvc';
import store from '../../store'; import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
let editorClassAppliers = []; let editorClassAppliers = [];
let previewClassAppliers = []; let previewClassAppliers = [];
@ -237,10 +238,12 @@ export default {
syncLocation: { syncLocation: {
immediate: true, immediate: true,
handler(value) { handler(value) {
if (!value) {
const firstSyncLocation = this.syncLocations[0]; const firstSyncLocation = this.syncLocations[0];
if (firstSyncLocation) { if (firstSyncLocation) {
if (!value) {
this.syncLocationId = firstSyncLocation.id; this.syncLocationId = firstSyncLocation.id;
} else if (value.id !== firstSyncLocation.id) {
badgeSvc.addBadge('chooseHistory');
} }
} }
}, },

View File

@ -53,6 +53,7 @@ import Provider from '../../services/providers/common/Provider';
import store from '../../store'; import store from '../../store';
import workspaceSvc from '../../services/workspaceSvc'; import workspaceSvc from '../../services/workspaceSvc';
import exportSvc from '../../services/exportSvc'; import exportSvc from '../../services/exportSvc';
import badgeSvc from '../../services/badgeSvc';
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown); const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
@ -85,6 +86,7 @@ export default {
name: file.name, name: file.name,
}); });
store.commit('file/setCurrentId', item.id); store.commit('file/setCurrentId', item.id);
badgeSvc.addBadge('importMarkdown');
}, },
async onImportHtml(evt) { async onImportHtml(evt) {
const file = evt.target.files[0]; const file = evt.target.files[0];
@ -96,23 +98,29 @@ export default {
name: file.name, name: file.name,
}); });
store.commit('file/setCurrentId', item.id); store.commit('file/setCurrentId', item.id);
badgeSvc.addBadge('importHtml');
}, },
exportMarkdown() { async exportMarkdown() {
const currentFile = store.getters['file/current']; const currentFile = store.getters['file/current'];
return exportSvc.exportToDisk(currentFile.id, 'md') try {
.catch(() => { /* Cancel */ }); await exportSvc.exportToDisk(currentFile.id, 'md');
badgeSvc.addBadge('exportMarkdown');
} catch (e) { /* Cancel */ }
}, },
exportHtml() { async exportHtml() {
return store.dispatch('modal/open', 'htmlExport') try {
.catch(() => { /* Cancel */ }); await store.dispatch('modal/open', 'htmlExport');
} catch (e) { /* Cancel */ }
}, },
exportPdf() { async exportPdf() {
return store.dispatch('modal/open', 'pdfExport') try {
.catch(() => { /* Cancel */ }); await store.dispatch('modal/open', 'pdfExport');
} catch (e) { /* Cancel */ }
}, },
exportPandoc() { async exportPandoc() {
return store.dispatch('modal/open', 'pandocExport') try {
.catch(() => { /* Cancel */ }); await store.dispatch('modal/open', 'pandocExport');
} catch (e) { /* Cancel */ }
}, },
}, },
}; };

View File

@ -99,15 +99,20 @@
<div><div class="menu-entry__label menu-entry__label--count">{{accountCount}}</div> User accounts</div> <div><div class="menu-entry__label menu-entry__label--count">{{accountCount}}</div> User accounts</div>
<span>Manage access to your external accounts.</span> <span>Manage access to your external accounts.</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="badges">
<icon-seal slot="icon"></icon-seal>
<div><div class="menu-entry__label menu-entry__label--count">{{badgeCount}}/{{featureCount}}</div> Badges</div>
<span>List application features and earned badges.</span>
</menu-entry>
<hr> <hr>
<menu-entry @click.native="setPanel('workspaceBackup')"> <menu-entry @click.native="setPanel('workspaceBackups')">
<icon-content-save slot="icon"></icon-content-save> <icon-content-save slot="icon"></icon-content-save>
Workspace backup Workspace backups
</menu-entry> </menu-entry>
<menu-entry @click.native="reset"> <menu-entry @click.native="reset">
<icon-logout slot="icon"></icon-logout> <icon-logout slot="icon"></icon-logout>
<div>Reset application</div> <div>Reset application</div>
<span>Sign out and clean all workspaces.</span> <span>Sign out and clean all workspace data.</span>
</menu-entry> </menu-entry>
<hr> <hr>
<menu-entry @click.native="about"> <menu-entry @click.native="about">
@ -161,6 +166,12 @@ export default {
return Object.values(store.getters['data/tokensByType']) return Object.values(store.getters['data/tokensByType'])
.reduce((count, tokensBySub) => count + Object.values(tokensBySub).length, 0); .reduce((count, tokensBySub) => count + Object.values(tokensBySub).length, 0);
}, },
badgeCount() {
return store.getters['data/allBadges'].filter(badge => badge.isEarned).length;
},
featureCount() {
return store.getters['data/allBadges'].length;
},
}, },
methods: { methods: {
...mapActions('data', { ...mapActions('data', {
@ -186,35 +197,30 @@ export default {
}, },
async settings() { async settings() {
try { try {
const settings = await store.dispatch('modal/open', 'settings'); await store.dispatch('modal/open', 'settings');
store.dispatch('data/setSettings', settings); } catch (e) { /* Cancel */ }
} catch (e) {
// Cancel
}
}, },
async templates() { async templates() {
try { try {
const { templates } = await store.dispatch('modal/open', 'templates'); await store.dispatch('modal/open', 'templates');
store.dispatch('data/setTemplatesById', templates); } catch (e) { /* Cancel */ }
} catch (e) {
// Cancel
}
}, },
async accounts() { async accounts() {
try { try {
await store.dispatch('modal/open', 'accountManagement'); await store.dispatch('modal/open', 'accountManagement');
} catch (e) { } catch (e) { /* Cancel */ }
// Cancel },
} async badges() {
try {
await store.dispatch('modal/open', 'badgeManagement');
} catch (e) { /* Cancel */ }
}, },
async reset() { async reset() {
try { try {
await store.dispatch('modal/open', 'reset'); await store.dispatch('modal/open', 'reset');
window.location.href = '#reset=true'; window.location.href = '#reset=true';
window.location.reload(); window.location.reload();
} catch (e) { } catch (e) { /* Cancel */ }
// Cancel
}
}, },
about() { about() {
store.dispatch('modal/open', 'about'); store.dispatch('modal/open', 'about');

View File

@ -129,13 +129,13 @@ const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
.filter(token => filter(token)) .filter(token => filter(token))
.sort((token1, token2) => token1.name.localeCompare(token2.name)); .sort((token1, token2) => token1.name.localeCompare(token2.name));
const publishModalOpener = type => async (token) => { const publishModalOpener = (type, featureId) => async (token) => {
try { try {
const publishLocation = await store.dispatch('modal/open', { const publishLocation = await store.dispatch('modal/open', {
type, type,
token, token,
}); });
publishSvc.createPublishLocation(publishLocation); publishSvc.createPublishLocation(publishLocation, featureId);
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}; };
@ -236,15 +236,15 @@ export default {
await zendeskHelper.addAccount(subdomain, clientId); await zendeskHelper.addAccount(subdomain, clientId);
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
publishBlogger: publishModalOpener('bloggerPublish'), publishBlogger: publishModalOpener('bloggerPublish', 'publishToBlogger'),
publishBloggerPage: publishModalOpener('bloggerPagePublish'), publishBloggerPage: publishModalOpener('bloggerPagePublish', 'publishToBloggerPage'),
publishDropbox: publishModalOpener('dropboxPublish'), publishDropbox: publishModalOpener('dropboxPublish', 'publishToDropbox'),
publishGithub: publishModalOpener('githubPublish'), publishGithub: publishModalOpener('githubPublish', 'publishToGithub'),
publishGist: publishModalOpener('gistPublish'), publishGist: publishModalOpener('gistPublish', 'publishToGist'),
publishGitlab: publishModalOpener('gitlabPublish'), publishGitlab: publishModalOpener('gitlabPublish', 'publishToGitlab'),
publishGoogleDrive: publishModalOpener('googleDrivePublish'), publishGoogleDrive: publishModalOpener('googleDrivePublish', 'publishToGoogleDrive'),
publishWordpress: publishModalOpener('wordpressPublish'), publishWordpress: publishModalOpener('wordpressPublish', 'publishToWordPress'),
publishZendesk: publishModalOpener('zendeskPublish'), publishZendesk: publishModalOpener('zendeskPublish', 'publishToZendesk'),
}, },
}; };
</script> </script>

View File

@ -108,6 +108,7 @@ import githubProvider from '../../services/providers/githubProvider';
import gitlabProvider from '../../services/providers/gitlabProvider'; import gitlabProvider from '../../services/providers/gitlabProvider';
import syncSvc from '../../services/syncSvc'; import syncSvc from '../../services/syncSvc';
import store from '../../store'; import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
const tokensToArray = (tokens, filter = () => true) => Object.values(tokens) const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
.filter(token => filter(token)) .filter(token => filter(token))
@ -162,7 +163,7 @@ export default {
methods: { methods: {
requestSync() { requestSync() {
if (!this.isSyncRequested) { if (!this.isSyncRequested) {
syncSvc.requestSync(); syncSvc.requestSync(true);
} }
}, },
async manageSync() { async manageSync() {
@ -194,28 +195,36 @@ export default {
await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess); await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
async openGoogleDrive(token) {
const files = await googleHelper.openPicker(token, 'doc');
store.dispatch(
'queue/enqueue',
() => googleDriveProvider.openFiles(token, files),
);
},
async openDropbox(token) { async openDropbox(token) {
const paths = await dropboxHelper.openChooser(token); const paths = await dropboxHelper.openChooser(token);
store.dispatch( store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => dropboxProvider.openFiles(token, paths), async () => {
await dropboxProvider.openFiles(token, paths);
badgeSvc.addBadge('openFromDropbox');
},
);
},
async saveDropbox(token) {
try {
await openSyncModal(token, 'dropboxSave');
badgeSvc.addBadge('saveOnDropbox');
} catch (e) { /* cancel */ }
},
async openGoogleDrive(token) {
const files = await googleHelper.openPicker(token, 'doc');
store.dispatch(
'queue/enqueue',
async () => {
await googleDriveProvider.openFiles(token, files);
badgeSvc.addBadge('openFromGoogleDrive');
},
); );
}, },
async saveGoogleDrive(token) { async saveGoogleDrive(token) {
try { try {
await openSyncModal(token, 'googleDriveSave'); await openSyncModal(token, 'googleDriveSave');
} catch (e) { /* cancel */ } badgeSvc.addBadge('saveOnGoogleDrive');
},
async saveDropbox(token) {
try {
await openSyncModal(token, 'dropboxSave');
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
async openGithub(token) { async openGithub(token) {
@ -226,18 +235,23 @@ export default {
}); });
store.dispatch( store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => githubProvider.openFile(token, syncLocation), async () => {
await githubProvider.openFile(token, syncLocation);
badgeSvc.addBadge('openFromGithub');
},
); );
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
async saveGithub(token) { async saveGithub(token) {
try { try {
await openSyncModal(token, 'githubSave'); await openSyncModal(token, 'githubSave');
badgeSvc.addBadge('saveOnGithub');
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
async saveGist(token) { async saveGist(token) {
try { try {
await openSyncModal(token, 'gistSync'); await openSyncModal(token, 'gistSync');
badgeSvc.addBadge('saveOnGist');
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
async openGitlab(token) { async openGitlab(token) {
@ -248,13 +262,17 @@ export default {
}); });
store.dispatch( store.dispatch(
'queue/enqueue', 'queue/enqueue',
() => gitlabProvider.openFile(token, syncLocation), async () => {
await gitlabProvider.openFile(token, syncLocation);
badgeSvc.addBadge('openFromGitlab');
},
); );
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
async saveGitlab(token) { async saveGitlab(token) {
try { try {
await openSyncModal(token, 'gitlabSave'); await openSyncModal(token, 'gitlabSave');
badgeSvc.addBadge('saveOnGitlab');
} catch (e) { /* cancel */ } } catch (e) { /* cancel */ }
}, },
}, },

View File

@ -2,8 +2,6 @@
<modal-inner class="modal__inner-1--about-modal" aria-label="About"> <modal-inner class="modal__inner-1--about-modal" aria-label="About">
<div class="modal__content"> <div class="modal__content">
<div class="logo-background"></div> <div class="logo-background"></div>
<small>© 2013-2019 Dock5 Software Ltd.<br>v{{version}}</small>
<hr>
StackEdit on <a target="_blank" href="https://github.com/benweet/stackedit/">GitHub</a> StackEdit on <a target="_blank" href="https://github.com/benweet/stackedit/">GitHub</a>
<br> <br>
<a target="_blank" href="https://github.com/benweet/stackedit/issues">Issue tracker</a> <a target="_blank" href="https://github.com/benweet/stackedit/releases">Changelog</a> <a target="_blank" href="https://github.com/benweet/stackedit/issues">Issue tracker</a> <a target="_blank" href="https://github.com/benweet/stackedit/releases">Changelog</a>
@ -13,12 +11,13 @@
<a target="_blank" href="https://community.stackedit.io/">Community</a> <a target="_blank" href="https://community.stackedit.io/c/how-to">Tutos and How To</a> <a target="_blank" href="https://community.stackedit.io/">Community</a> <a target="_blank" href="https://community.stackedit.io/c/how-to">Tutos and How To</a>
<br> <br>
StackEdit on <a target="_blank" href="https://twitter.com/stackedit/">Twitter</a> StackEdit on <a target="_blank" href="https://twitter.com/stackedit/">Twitter</a>
<div class="modal__info"> <hr>
For commercial support or custom development, please <a href="mailto:stackedit.project@gmail.com">send us an email</a>. <small>© 2013-2019 Dock5 Software Ltd.<br>v{{version}}</small>
</div>
<h3>FAQ</h3> <h3>FAQ</h3>
<div class="faq" v-html="faq"></div> <div class="faq" v-html="faq"></div>
<hr> <div class="modal__info">
For commercial support or custom development, please <a href="mailto:stackedit.project@gmail.com">contact us</a>.
</div>
Licensed under an Licensed under an
<a target="_blank" href="http://www.apache.org/licenses/LICENSE-2.0">Apache License</a><br> <a target="_blank" href="http://www.apache.org/licenses/LICENSE-2.0">Apache License</a><br>
<a target="_blank" href="privacy_policy.html">Privacy Policy</a> <a target="_blank" href="privacy_policy.html">Privacy Policy</a>

View File

@ -88,6 +88,7 @@ import githubHelper from '../../services/providers/helpers/githubHelper';
import gitlabHelper from '../../services/providers/helpers/gitlabHelper'; import gitlabHelper from '../../services/providers/helpers/gitlabHelper';
import wordpressHelper from '../../services/providers/helpers/wordpressHelper'; import wordpressHelper from '../../services/providers/helpers/wordpressHelper';
import zendeskHelper from '../../services/providers/helpers/zendeskHelper'; import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
import badgeSvc from '../../services/badgeSvc';
export default { export default {
components: { components: {
@ -160,6 +161,7 @@ export default {
await store.dispatch('data/patchTokensByType', { await store.dispatch('data/patchTokensByType', {
[entry.providerId]: tokensBySub, [entry.providerId]: tokensBySub,
}); });
badgeSvc.addBadge('removeAccount');
}, },
async addBloggerAccount() { async addBloggerAccount() {
try { try {

View File

@ -0,0 +1,109 @@
<template>
<modal-inner class="modal__inner-1--badge-management" aria-label="Manage badges">
<div class="modal__content">
<div class="modal__image">
<icon-seal></icon-seal>
</div>
<p v-if="badgeCount > 1">{{badgeCount}} badges earned</p>
<p v-else>{{badgeCount}} badge earned</p>
<div class="badge-entry" :class="{'badge-entry--earned': badge.isEarned}" v-for="badge in badgeTree" :key="badge.featureId">
<div class="flex flex--row">
<icon-seal></icon-seal>
<div>
<span class="badge-entry__name">{{badge.name}}</span>
<span class="badge-entry__description">&mdash; {{badge.description}}</span>
<div class="badge-entry" :class="{'badge-entry--earned': child.isEarned}" v-for="child in badge.children" :key="child.featureId">
<div class="flex flex--row">
<icon-seal></icon-seal>
<div>
<span class="badge-entry__name">{{child.name}}</span>
<span class="badge-entry__description">&mdash; {{child.description}}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal__button-bar">
<button class="button button--resolve" @click="config.resolve()">Close</button>
</div>
</modal-inner>
</template>
<script>
import { mapGetters } from 'vuex';
import ModalInner from './common/ModalInner';
import store from '../../store';
export default {
components: {
ModalInner,
},
computed: {
...mapGetters('modal', [
'config',
]),
...mapGetters('data', [
'badgeTree',
]),
badgeCount() {
return store.getters['data/allBadges'].filter(badge => badge.isEarned).length;
},
featureCount() {
return store.getters['data/allBadges'].length;
},
},
};
</script>
<style lang="scss">
@import '../../styles/variables.scss';
.modal__inner-1.modal__inner-1--badge-management {
max-width: 520px;
p {
font-size: 1.8rem;
font-weight: bold;
}
}
.badge-entry {
line-height: 1.4;
margin: 2rem 0;
font-size: 0.9em;
.badge-entry {
font-size: 0.8em;
margin: 0.75rem 0 0;
}
svg {
width: 1.67em;
height: 1.67em;
margin-right: 0.25em;
opacity: 0.33;
flex: none;
}
}
.badge-entry--earned svg {
opacity: 1;
color: goldenrod;
}
.badge-entry__description {
opacity: 0.6;
}
.badge-entry__name {
font-size: 1.2em;
font-weight: bold;
opacity: 0.5;
.badge-entry--earned & {
opacity: 1;
}
}
</style>

View File

@ -93,8 +93,9 @@ import CodeEditor from '../CodeEditor';
import utils from '../../services/utils'; import utils from '../../services/utils';
import presets from '../../data/presets'; import presets from '../../data/presets';
import store from '../../store'; import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
const simpleProperties = { const metadataProperties = {
title: '', title: '',
author: '', author: '',
tags: '', tags: '',
@ -117,7 +118,7 @@ export default {
yamlProperties: null, yamlProperties: null,
preset: '', preset: '',
error: null, error: null,
...simpleProperties, ...metadataProperties,
}), }),
computed: { computed: {
...mapGetters('modal', [ ...mapGetters('modal', [
@ -148,10 +149,10 @@ export default {
const properties = this.properties || {}; const properties = this.properties || {};
const extensions = properties.extensions || {}; const extensions = properties.extensions || {};
this.preset = extensions.preset; this.preset = extensions.preset;
if (this.presets.indexOf(this.preset) === -1) { if (!this.presets.includes(this.preset)) {
this.preset = 'default'; this.preset = 'default';
} }
Object.keys(simpleProperties).forEach((name) => { Object.keys(metadataProperties).forEach((name) => {
this[name] = `${properties[name] || ''}`; this[name] = `${properties[name] || ''}`;
}); });
}, },
@ -168,7 +169,7 @@ export default {
hasChanged = true; hasChanged = true;
} }
} }
Object.keys(simpleProperties).forEach((name) => { Object.keys(metadataProperties).forEach((name) => {
if (this[name] !== properties[name]) { if (this[name] !== properties[name]) {
if (this[name]) { if (this[name]) {
properties[name] = this[name]; properties[name] = this[name];
@ -215,6 +216,17 @@ export default {
if (this.error) { if (this.error) {
this.setYamlTab(); this.setYamlTab();
} else { } else {
const properties = this.properties || {};
if (Object.keys(metadataProperties).some(key => properties[key])) {
badgeSvc.addBadge('setMetadata');
}
const extensions = properties.extensions || {};
if (extensions.preset) {
badgeSvc.addBadge('changePreset');
}
if (Object.keys(extensions).filter(key => key !== 'preset').length) {
badgeSvc.addBadge('changeExtension');
}
store.commit('content/patchItem', { store.commit('content/patchItem', {
id: this.contentId, id: this.contentId,
properties: utils.sanitizeText(this.yamlProperties), properties: utils.sanitizeText(this.yamlProperties),

View File

@ -26,6 +26,7 @@ import { mapActions } from 'vuex';
import exportSvc from '../../services/exportSvc'; import exportSvc from '../../services/exportSvc';
import modalTemplate from './common/modalTemplate'; import modalTemplate from './common/modalTemplate';
import store from '../../store'; import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
export default modalTemplate({ export default modalTemplate({
data: () => ({ data: () => ({
@ -54,11 +55,12 @@ export default modalTemplate({
...mapActions('notification', [ ...mapActions('notification', [
'info', 'info',
]), ]),
resolve() { async resolve() {
const { config } = this; const { config } = this;
const currentFile = store.getters['file/current']; const currentFile = store.getters['file/current'];
config.resolve(); config.resolve();
exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplatesById[this.selectedTemplate]); await exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplatesById[this.selectedTemplate]);
badgeSvc.addBadge('exportHtml');
}, },
}, },
}); });

View File

@ -27,12 +27,12 @@
<script> <script>
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import sponsorSvc from '../../services/sponsorSvc';
import networkSvc from '../../services/networkSvc'; import networkSvc from '../../services/networkSvc';
import editorSvc from '../../services/editorSvc'; import editorSvc from '../../services/editorSvc';
import googleHelper from '../../services/providers/helpers/googleHelper'; import googleHelper from '../../services/providers/helpers/googleHelper';
import modalTemplate from './common/modalTemplate'; import modalTemplate from './common/modalTemplate';
import store from '../../store'; import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
export default modalTemplate({ export default modalTemplate({
computedLocalSettings: { computedLocalSettings: {
@ -45,20 +45,14 @@ export default modalTemplate({
const currentContent = store.getters['content/current']; const currentContent = store.getters['content/current'];
const { selectedFormat } = this; const { selectedFormat } = this;
store.dispatch('queue/enqueue', async () => { store.dispatch('queue/enqueue', async () => {
const [sponsorToken, token] = await Promise.all([
Promise.resolve().then(() => {
const tokenToRefresh = store.getters['workspace/sponsorToken']; const tokenToRefresh = store.getters['workspace/sponsorToken'];
return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh); const sponsorToken = tokenToRefresh && await googleHelper.refreshToken(tokenToRefresh);
}),
sponsorSvc.getToken(),
]);
try { try {
const { body } = await networkSvc.request({ const { body } = await networkSvc.request({
method: 'POST', method: 'POST',
url: 'pandocExport', url: 'pandocExport',
params: { params: {
token,
idToken: sponsorToken && sponsorToken.idToken, idToken: sponsorToken && sponsorToken.idToken,
format: selectedFormat, format: selectedFormat,
options: JSON.stringify(store.getters['data/computedSettings'].pandoc), options: JSON.stringify(store.getters['data/computedSettings'].pandoc),
@ -69,6 +63,7 @@ export default modalTemplate({
timeout: 60000, timeout: 60000,
}); });
FileSaver.saveAs(body, `${currentFile.name}.${selectedFormat}`); FileSaver.saveAs(body, `${currentFile.name}.${selectedFormat}`);
badgeSvc.addBadge('exportPandoc');
} catch (err) { } catch (err) {
if (err.status === 401) { if (err.status === 401) {
store.dispatch('modal/open', 'sponsorOnly'); store.dispatch('modal/open', 'sponsorOnly');

View File

@ -23,11 +23,11 @@
<script> <script>
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import exportSvc from '../../services/exportSvc'; import exportSvc from '../../services/exportSvc';
import sponsorSvc from '../../services/sponsorSvc';
import networkSvc from '../../services/networkSvc'; import networkSvc from '../../services/networkSvc';
import googleHelper from '../../services/providers/helpers/googleHelper'; import googleHelper from '../../services/providers/helpers/googleHelper';
import modalTemplate from './common/modalTemplate'; import modalTemplate from './common/modalTemplate';
import store from '../../store'; import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
export default modalTemplate({ export default modalTemplate({
computedLocalSettings: { computedLocalSettings: {
@ -38,12 +38,11 @@ export default modalTemplate({
this.config.resolve(); this.config.resolve();
const currentFile = store.getters['file/current']; const currentFile = store.getters['file/current'];
store.dispatch('queue/enqueue', async () => { store.dispatch('queue/enqueue', async () => {
const [sponsorToken, token, html] = await Promise.all([ const [sponsorToken, html] = await Promise.all([
Promise.resolve().then(() => { Promise.resolve().then(() => {
const tokenToRefresh = store.getters['workspace/sponsorToken']; const tokenToRefresh = store.getters['workspace/sponsorToken'];
return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh); return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
}), }),
sponsorSvc.getToken(),
exportSvc.applyTemplate( exportSvc.applyTemplate(
currentFile.id, currentFile.id,
this.allTemplatesById[this.selectedTemplate], this.allTemplatesById[this.selectedTemplate],
@ -56,7 +55,6 @@ export default modalTemplate({
method: 'POST', method: 'POST',
url: 'pdfExport', url: 'pdfExport',
params: { params: {
token,
idToken: sponsorToken && sponsorToken.idToken, idToken: sponsorToken && sponsorToken.idToken,
options: JSON.stringify(store.getters['data/computedSettings'].wkhtmltopdf), options: JSON.stringify(store.getters['data/computedSettings'].wkhtmltopdf),
}, },
@ -65,6 +63,7 @@ export default modalTemplate({
timeout: 60000, timeout: 60000,
}); });
FileSaver.saveAs(body, `${currentFile.name}.pdf`); FileSaver.saveAs(body, `${currentFile.name}.pdf`);
badgeSvc.addBadge('exportPdf');
} catch (err) { } catch (err) {
if (err.status === 401) { if (err.status === 401) {
store.dispatch('modal/open', 'sponsorOnly'); store.dispatch('modal/open', 'sponsorOnly');

View File

@ -50,6 +50,7 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import ModalInner from './common/ModalInner'; import ModalInner from './common/ModalInner';
import store from '../../store'; import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
export default { export default {
components: { components: {
@ -72,6 +73,7 @@ export default {
]), ]),
remove(location) { remove(location) {
store.commit('publishLocation/deleteItem', location.id); store.commit('publishLocation/deleteItem', location.id);
badgeSvc.addBadge('removePublishLocation');
}, },
}, },
}; };

View File

@ -25,7 +25,7 @@
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
<button class="button" @click="config.reject()">Cancel</button> <button class="button" @click="config.reject()">Cancel</button>
<button class="button button--resolve" @click="!error && config.resolve(strippedCustomSettings)">Ok</button> <button class="button button--resolve" @click="resolve">Ok</button>
</div> </div>
</modal-inner> </modal-inner>
</template> </template>
@ -38,6 +38,7 @@ import Tab from './common/Tab';
import CodeEditor from '../CodeEditor'; import CodeEditor from '../CodeEditor';
import defaultSettings from '../../data/defaults/defaultSettings.yml'; import defaultSettings from '../../data/defaults/defaultSettings.yml';
import store from '../../store'; import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
const emptySettings = `# Add your custom settings here to override the const emptySettings = `# Add your custom settings here to override the
# default settings. # default settings.
@ -77,6 +78,25 @@ export default {
this.error = e.message; this.error = e.message;
} }
}, },
async resolve() {
if (!this.error) {
const settings = this.strippedCustomSettings;
await store.dispatch('data/setSettings', settings);
const customSettings = yaml.safeLoad(settings);
if (customSettings.shortcuts) {
badgeSvc.addBadge('changeShortcuts');
}
const computedSettings = store.getters['data/computedSettings'];
const customSettingsCount = Object
.keys(customSettings)
.filter(key => key !== 'shortcuts' && computedSettings[key])
.length;
if (customSettingsCount) {
badgeSvc.addBadge('changeSettings');
}
this.config.resolve(settings);
}
},
}, },
}; };
</script> </script>

View File

@ -50,6 +50,7 @@
import { mapGetters, mapActions } from 'vuex'; import { mapGetters, mapActions } from 'vuex';
import ModalInner from './common/ModalInner'; import ModalInner from './common/ModalInner';
import store from '../../store'; import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
export default { export default {
components: { components: {
@ -75,6 +76,7 @@ export default {
this.info('This location can not be removed.'); this.info('This location can not be removed.');
} else { } else {
store.commit('syncLocation/deleteItem', location.id); store.commit('syncLocation/deleteItem', location.id);
badgeSvc.addBadge('removeSyncLocation');
} }
}, },
}, },

View File

@ -53,6 +53,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import utils from '../../services/utils'; import utils from '../../services/utils';
import badgeSvc from '../../services/badgeSvc';
import ModalInner from './common/ModalInner'; import ModalInner from './common/ModalInner';
import CodeEditor from '../CodeEditor'; import CodeEditor from '../CodeEditor';
import emptyTemplateValue from '../../data/empties/emptyTemplateValue.html'; import emptyTemplateValue from '../../data/empties/emptyTemplateValue.html';
@ -153,7 +154,22 @@ export default {
this.isEditing = false; this.isEditing = false;
}, 1); }, 1);
}, },
resolve() { async resolve() {
const oldTemplateIds = Object.keys(store.getters['data/templatesById']);
await store.dispatch('data/setTemplatesById', this.templates);
const newTemplateIds = Object.keys(store.getters['data/templatesById']);
const createdCount = newTemplateIds
.filter(id => !oldTemplateIds.includes(id))
.length;
const removedCount = oldTemplateIds
.filter(id => !newTemplateIds.includes(id))
.length;
if (createdCount) {
badgeSvc.addBadge('addTemplate');
}
if (removedCount) {
badgeSvc.addBadge('removeTemplate');
}
this.config.resolve({ this.config.resolve({
templates: this.templates, templates: this.templates,
selectedId: this.selectedId, selectedId: this.selectedId,

View File

@ -65,6 +65,7 @@ import { mapGetters, mapActions } from 'vuex';
import ModalInner from './common/ModalInner'; import ModalInner from './common/ModalInner';
import workspaceSvc from '../../services/workspaceSvc'; import workspaceSvc from '../../services/workspaceSvc';
import store from '../../store'; import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
export default { export default {
components: { components: {
@ -95,13 +96,14 @@ export default {
submitEdit(cancel) { submitEdit(cancel) {
const workspace = this.workspacesById[this.editedId]; const workspace = this.workspacesById[this.editedId];
if (workspace) { if (workspace) {
if (!cancel && this.editingName) { if (!cancel && this.editingName && this.editingName !== workspace.name) {
store.dispatch('workspace/patchWorkspacesById', { store.dispatch('workspace/patchWorkspacesById', {
[this.editedId]: { [this.editedId]: {
...workspace, ...workspace,
name: this.editingName, name: this.editingName,
}, },
}); });
badgeSvc.addBadge('renameWorkspace');
} else { } else {
this.editingName = workspace.name; this.editingName = workspace.name;
} }
@ -117,6 +119,7 @@ export default {
try { try {
await store.dispatch('modal/open', 'removeWorkspace'); await store.dispatch('modal/open', 'removeWorkspace');
workspaceSvc.removeWorkspace(id); workspaceSvc.removeWorkspace(id);
badgeSvc.addBadge('removeWorkspace');
} catch (e) { /* Cancel */ } } catch (e) { /* Cancel */ }
} }
}, },

View File

@ -64,11 +64,10 @@ export default (desc) => {
}; };
// Make use of `function` to have `this` bound to the component // Make use of `function` to have `this` bound to the component
component.methods.configureTemplates = async function () { // eslint-disable-line func-names component.methods.configureTemplates = async function () { // eslint-disable-line func-names
const { templates, selectedId } = await store.dispatch('modal/open', { const { selectedId } = await store.dispatch('modal/open', {
type: 'templates', type: 'templates',
selectedId: this.selectedTemplate, selectedId: this.selectedTemplate,
}); });
store.dispatch('data/setTemplatesById', templates);
store.dispatch('data/patchLocalSettings', { store.dispatch('data/patchLocalSettings', {
[id]: selectedId, [id]: selectedId,
}); });

View File

@ -19,6 +19,7 @@ export default {
'settings', 'settings',
'layoutSettings', 'layoutSettings',
'tokens', 'tokens',
'badges',
], ],
textMaxLength: 250000, textMaxLength: 250000,
defaultName: 'Untitled', defaultName: 'Untitled',

466
src/data/features.js Normal file
View File

@ -0,0 +1,466 @@
class Badge {
constructor(featureId, name, description, children, isEarned) {
this.featureId = featureId;
this.name = name;
this.description = description;
this.children = children;
this.isEarned = isEarned;
}
}
class Feature {
constructor(id, badgeName, description, children = null) {
this.id = id;
this.badgeName = badgeName;
this.description = description;
this.children = children;
}
toBadge(earnings) {
const children = this.children
? this.children.map(child => child.toBadge(earnings))
: null;
return new Badge(this.id, this.badgeName, this.description, children, children
? children.every(child => child.isEarned)
: !!earnings[this.id]);
}
}
export default [
new Feature(
'navigationBar',
'Nav bar expert',
'Master the navigation bar by formatting some Markdown and renaming the current file.',
[
new Feature(
'formatButtons',
'Formatter',
'Use the format buttons to change formatting in your Markdown file.',
),
new Feature(
'editCurrentFileName',
'Renamer',
'Use the name field in the navigation bar to rename the current file.',
),
],
),
new Feature(
'explorer',
'Explorer',
'Use the file explorer to manage files and folders in your workspace.',
[
new Feature(
'createFile',
'File creator',
'Use the file explorer to create a new file in your workspace.',
),
new Feature(
'switchFile',
'File switcher',
'Use the file explorer to switch from one file to another in your workspace.',
),
new Feature(
'createFolder',
'Folder creator',
'Use the file explorer to create a new folder in your workspace.',
),
new Feature(
'moveFiles',
'File mover',
'Drag files in the file explorer to move them around.',
),
new Feature(
'renameFile',
'File renamer',
'Use the file explorer to rename a file in your workspace.',
),
new Feature(
'renameFolder',
'Folder renamer',
'Use the file explorer to rename a folder in your workspace.',
),
new Feature(
'removeFiles',
'File remover',
'Use the file explorer to remove files in your workspace.',
),
],
),
new Feature(
'signIn',
'Logged in',
'Sign in with Google, sync your main workspace and unlock functionalities.',
[
new Feature(
'syncMainWorkspace',
'Main workspace synced',
'Sign in with Google to sync your main workspace with your Google Drive app data folder.',
),
new Feature(
'sponsor',
'Sponsor',
'Sign in with Google and sponsor StackEdit to unlock PDF and Pandoc exports.',
),
],
),
new Feature(
'workspaces',
'Workspace expert',
'Use the workspace menu to create all kinds of workspaces and to manage them.',
[
new Feature(
'addCouchdbWorkspace',
'CouchDB workspace creator',
'Use the workspace menu to create a CouchDB workspace.',
),
new Feature(
'addGithubWorkspace',
'GitHub workspace creator',
'Use the workspace menu to create a GitHub workspace.',
),
new Feature(
'addGitlabWorkspace',
'GitLab workspace creator',
'Use the workspace menu to create a GitLab workspace.',
),
new Feature(
'addGoogleDriveWorkspace',
'Google Drive workspace creator',
'Use the workspace menu to create a Google Drive workspace.',
),
new Feature(
'renameWorkspace',
'Workspace renamer',
'Use the "Manage workspaces" dialog to rename a workspace.',
),
new Feature(
'removeWorkspace',
'Workspace remover',
'Use the "Manage workspaces" dialog to remove a workspace locally.',
),
],
),
new Feature(
'manageAccounts',
'Account manager',
'Link all kinds of external accounts and use the "User accounts" dialog to manage them.',
[
new Feature(
'addBloggerAccount',
'Blogger user',
'Link your Blogger account to StackEdit.',
),
new Feature(
'addDropboxAccount',
'Dropbox user',
'Link your Dropbox account to StackEdit.',
),
new Feature(
'addGitHubAccount',
'GitHub user',
'Link your GitHub account to StackEdit.',
),
new Feature(
'addGitLabAccount',
'GitLab user',
'Link your GitLab account to StackEdit.',
),
new Feature(
'addGoogleDriveAccount',
'Google Drive user',
'Link your Google Drive account to StackEdit.',
),
new Feature(
'addGooglePhotosAccount',
'Google Photos user',
'Link your Google Photos account to StackEdit.',
),
new Feature(
'addWordpressAccount',
'WordPress user',
'Link your WordPress account to StackEdit.',
),
new Feature(
'addZendeskAccount',
'Zendesk user',
'Link your Zendesk account to StackEdit.',
),
new Feature(
'removeAccount',
'Revoker',
'Use the "User accounts" dialog to remove access to an external account.',
),
],
),
new Feature(
'syncFiles',
'File synchronizer',
'Master the "Synchronize" menu by opening and saving files with all kinds of external accounts.',
[
new Feature(
'openFromDropbox',
'Dropbox reader',
'Use the "Synchronize" menu to open a file from your Dropbox account.',
),
new Feature(
'saveOnDropbox',
'Dropbox writer',
'Use the "Synchronize" menu to save a file in your Dropbox account.',
),
new Feature(
'openFromGithub',
'GitHub reader',
'Use the "Synchronize" menu to open a file from a GitHub repository.',
),
new Feature(
'saveOnGithub',
'GitHub writer',
'Use the "Synchronize" menu to save a file in a GitHub repository.',
),
new Feature(
'saveOnGist',
'Gist writer',
'Use the "Synchronize" menu to save a file in a Gist.',
),
new Feature(
'openFromGitlab',
'GitLab reader',
'Use the "Synchronize" menu to open a file from a GitLab repository.',
),
new Feature(
'saveOnGitlab',
'GitLab writer',
'Use the "Synchronize" menu to save a file in a GitLab repository.',
),
new Feature(
'openFromGoogleDrive',
'Google Drive reader',
'Use the "Synchronize" menu to open a file from your Google Drive account.',
),
new Feature(
'saveOnGoogleDrive',
'Google Drive writer',
'Use the "Synchronize" menu to save a file in your Google Drive account.',
),
new Feature(
'triggerSync',
'Sync trigger',
'Use the "Synchronize" menu or the navigation bar to manually trigger synchronization.',
),
new Feature(
'syncMultipleLocations',
'Multi-sync',
'Use the "Synchronize" menu to synchronize a file with multiple external locations.',
),
new Feature(
'removeSyncLocation',
'Desynchronizer',
'Use the "File synchronization" dialog to remove a sync location.',
),
],
),
new Feature(
'publishFiles',
'File publisher',
'Master the "Publish" menu by publishing files to all kinds of external accounts.',
[
new Feature(
'publishToBlogger',
'Blogger publisher',
'Use the "Publish" menu to publish a Blogger article.',
),
new Feature(
'publishToBloggerPage',
'Blogger Page publisher',
'Use the "Publish" menu to publish a Blogger page.',
),
new Feature(
'publishToDropbox',
'Dropbox publisher',
'Use the "Publish" menu to publish a file to your Dropbox account.',
),
new Feature(
'publishToGithub',
'GitHub publisher',
'Use the "Publish" menu to publish a file to a GitHub repository.',
),
new Feature(
'publishToGist',
'Gist publisher',
'Use the "Publish" menu to publish a file to a Gist.',
),
new Feature(
'publishToGitlab',
'GitLab publisher',
'Use the "Publish" menu to publish a file to a GitLab repository.',
),
new Feature(
'publishToGoogleDrive',
'Google Drive publisher',
'Use the "Publish" menu to publish a file to your Google Drive account.',
),
new Feature(
'publishToWordPress',
'WordPress publisher',
'Use the "Publish" menu to publish a WordPress article.',
),
new Feature(
'publishToZendesk',
'Zendesk publisher',
'Use the "Publish" menu to publish a Zendesk Help Center article.',
),
new Feature(
'triggerPublish',
'Publication reviser',
'Use the "Publish" menu or the navigation bar to manually update publications.',
),
new Feature(
'publishMultipleLocations',
'Multi-publication',
'Use the "Publish" menu to publish a file to multiple external locations.',
),
new Feature(
'removePublishLocation',
'Unpublisher',
'Use the "File publication" dialog to remove a publish location.',
),
],
),
new Feature(
'manageHistory',
'Historian',
'Use the "File history" menu to see version history and restore old versions of the current file.',
[
new Feature(
'restoreVersion',
'Restorer',
'Use the "File history" menu to restore an old version of the current file.',
),
new Feature(
'chooseHistory',
'History chooser',
'Select a different history for a file that is synced with multiple external locations.',
),
],
),
new Feature(
'manageProperties',
'Property expert',
'Use the "File properties" dialog to change properties for the current file.',
[
new Feature(
'setMetadata',
'Metadata setter',
'Use the "File properties" dialog to set metadata for the current file.',
),
new Feature(
'changePreset',
'Preset changer',
'Use the "File properties" dialog to change the Markdown engine preset.',
),
new Feature(
'changeExtension',
'Extension expert',
'Use the "File properties" dialog to enable, disable or configure Markdown engine extensions.',
),
],
),
new Feature(
'comment',
'Comment expert',
'Start and remove discussions, add and remove comments.',
[
new Feature(
'createDiscussion',
'Discussion starter',
'Use the "comment" button to start a new discussion.',
),
new Feature(
'addComment',
'Commenter',
'Use the discussion gutter to add a comment to an existing discussion.',
),
new Feature(
'removeComment',
'Moderator',
'Use the discussion gutter to remove a comment in a discussion.',
),
new Feature(
'removeDiscussion',
'Discussion closer',
'Use the discussion gutter to remove a discussion.',
),
],
),
new Feature(
'importExport',
'Import/export',
'Use the "Import/export" menu to import and export files.',
[
new Feature(
'importMarkdown',
'Markdown importer',
'Use the "Import/export" menu to import a Markdown file from disk.',
),
new Feature(
'exportMarkdown',
'Markdown exporter',
'Use the "Import/export" menu to export a Markdown file to disk.',
),
new Feature(
'importHtml',
'HTML importer',
'Use the "Import/export" menu to import an HTML file from disk and convert it to Markdown.',
),
new Feature(
'exportHtml',
'HTML exporter',
'Use the "Import/export" menu to export a file to disk as an HTML file using a Handlebars template.',
),
new Feature(
'exportPdf',
'PDF exporter',
'Use the "Import/export" menu to export a file to disk as a PDF file.',
),
new Feature(
'exportPandoc',
'Pandoc exporter',
'Use the "Import/export" menu to export a file to disk using Pandoc.',
),
],
),
new Feature(
'manageSettings',
'Settings expert',
'Use the "Settings" dialog to tweak the application behaviors and change keyboard shortcuts.',
[
new Feature(
'changeSettings',
'Tweaker',
'Use the "Settings" dialog to tweak the application behaviors.',
),
new Feature(
'changeShortcuts',
'Shortcut editor',
'Use the "Settings" dialog to change keyboard shortcuts.',
),
],
),
new Feature(
'manageTemplates',
'Template expert',
'Use the "Templates" dialog to create, remove or modify Handlebars templates.',
[
new Feature(
'addTemplate',
'Template creator',
'Use the "Templates" dialog to create a Handlebars template.',
),
new Feature(
'removeTemplate',
'Template remover',
'Use the "Templates" dialog to remove a Handlebars template.',
),
],
),
];

5
src/icons/Seal.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="1 1 23 23">
<path d="M 20.3943,19.3706L 16.3828,17.9893L 15.0016,22.0008L 11.9248,15.9996L 8.99895,21.9986L 7.61768,17.9871L 3.60619,19.3683L 6.53159,13.3704C 5.57315,12.1727 5,10.6533 5,9C 5,5.13401 8.13401,2 12,2C 15.866,2 19,5.13401 19,9C 19,10.6535 18.4267,12.1731 17.468,13.3708L 20.3943,19.3706 Z M 7,9.00001L 9.68578,10.3429L 9.50615,13.3356L 12.012,11.6811L 14.514,13.333L 14.334,10.3356L 17.0156,8.9948L 14.323,7.64851L 14.5017,4.6727L 12.0162,6.31371L 9.49384,4.64828L 9.67477,7.66262L 7,9.00001 Z "/>
</svg>
</template>

View File

@ -53,6 +53,7 @@ import CheckCircle from './CheckCircle';
import ContentCopy from './ContentCopy'; import ContentCopy from './ContentCopy';
import Key from './Key'; import Key from './Key';
import DotsHorizontal from './DotsHorizontal'; import DotsHorizontal from './DotsHorizontal';
import Seal from './Seal';
Vue.component('iconProvider', Provider); Vue.component('iconProvider', Provider);
Vue.component('iconFormatBold', FormatBold); Vue.component('iconFormatBold', FormatBold);
@ -108,3 +109,4 @@ Vue.component('iconCheckCircle', CheckCircle);
Vue.component('iconContentCopy', ContentCopy); Vue.component('iconContentCopy', ContentCopy);
Vue.component('iconKey', Key); Vue.component('iconKey', Key);
Vue.component('iconDotsHorizontal', DotsHorizontal); Vue.component('iconDotsHorizontal', DotsHorizontal);
Vue.component('iconSeal', Seal);

37
src/services/badgeSvc.js Normal file
View File

@ -0,0 +1,37 @@
import store from '../store';
let lastEarnedFeatureIds = null;
let debounceTimeoutId;
const showInfo = () => {
const earnedBadges = store.getters['data/allBadges']
.filter(badge => badge.isEarned && !lastEarnedFeatureIds.has(badge.featureId));
if (earnedBadges.length) {
store.dispatch('notification/badge', earnedBadges.length > 1
? `You've earned ${earnedBadges.length} badges: ${earnedBadges.map(badge => `"${badge.name}"`).join(', ')}.`
: `You've earned 1 badge: "${earnedBadges[0].name}".`);
}
lastEarnedFeatureIds = null;
};
export default {
addBadge(featureId) {
if (!store.getters['data/badges'][featureId]) {
if (!lastEarnedFeatureIds) {
const earnedFeatureIds = store.getters['data/allBadges']
.filter(badge => badge.isEarned)
.map(badge => badge.featureId);
lastEarnedFeatureIds = new Set(earnedFeatureIds);
}
store.dispatch('data/patchBadges', {
[featureId]: {
created: Date.now(),
},
});
clearTimeout(debounceTimeoutId);
debounceTimeoutId = setTimeout(() => showInfo(), 5000);
}
},
};

View File

@ -1,5 +1,6 @@
import store from '../store'; import store from '../store';
import workspaceSvc from './workspaceSvc'; import workspaceSvc from './workspaceSvc';
import badgeSvc from './badgeSvc';
export default { export default {
newItem(isFolder = false) { newItem(isFolder = false) {
@ -61,6 +62,7 @@ export default {
} else { } else {
workspaceSvc.deleteFile(id); workspaceSvc.deleteFile(id);
} }
badgeSvc.addBadge('removeFiles');
}; };
if (selectedNode === store.getters['explorer/selectedNode']) { if (selectedNode === store.getters['explorer/selectedNode']) {

View File

@ -2,6 +2,7 @@ import store from '../../store';
import couchdbHelper from './helpers/couchdbHelper'; import couchdbHelper from './helpers/couchdbHelper';
import Provider from './common/Provider'; import Provider from './common/Provider';
import utils from '../utils'; import utils from '../utils';
import badgeSvc from '../badgeSvc';
let syncLastSeq; let syncLastSeq;
@ -58,6 +59,7 @@ export default new Provider({
} }
} }
badgeSvc.addBadge('addCouchdbWorkspace');
return store.getters['workspace/workspacesById'][workspaceId]; return store.getters['workspace/workspacesById'][workspaceId];
}, },
async getChanges() { async getChanges() {

View File

@ -66,7 +66,7 @@ export default new Provider({
return entries.map((entry) => { return entries.map((entry) => {
const sub = `${githubHelper.subPrefix}:${entry.user.id}`; const sub = `${githubHelper.subPrefix}:${entry.user.id}`;
userSvc.addInfo({ id: sub, name: entry.user.login, imageUrl: entry.user.avatar_url }); userSvc.addUserInfo({ id: sub, name: entry.user.login, imageUrl: entry.user.avatar_url });
return { return {
sub, sub,
id: entry.version, id: entry.version,

View File

@ -135,7 +135,7 @@ export default new Provider({
user = committer; user = committer;
} }
const sub = `${githubHelper.subPrefix}:${user.id}`; const sub = `${githubHelper.subPrefix}:${user.id}`;
userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url }); userSvc.addUserInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
const date = (commit.author && commit.author.date) const date = (commit.author && commit.author.date)
|| (commit.committer && commit.committer.date); || (commit.committer && commit.committer.date);
return { return {

View File

@ -4,6 +4,7 @@ import Provider from './common/Provider';
import utils from '../utils'; import utils from '../utils';
import userSvc from '../userSvc'; import userSvc from '../userSvc';
import gitWorkspaceSvc from '../gitWorkspaceSvc'; import gitWorkspaceSvc from '../gitWorkspaceSvc';
import badgeSvc from '../badgeSvc';
const getAbsolutePath = ({ id }) => const getAbsolutePath = ({ id }) =>
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`; `${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
@ -86,6 +87,7 @@ export default new Provider({
}); });
} }
badgeSvc.addBadge('addGithubWorkspace');
return store.getters['workspace/workspacesById'][workspaceId]; return store.getters['workspace/workspacesById'][workspaceId];
}, },
getChanges() { getChanges() {
@ -247,7 +249,7 @@ export default new Provider({
user = committer; user = committer;
} }
const sub = `${githubHelper.subPrefix}:${user.id}`; const sub = `${githubHelper.subPrefix}:${user.id}`;
userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url }); userSvc.addUserInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
const date = (commit.author && commit.author.date) const date = (commit.author && commit.author.date)
|| (commit.committer && commit.committer.date) || (commit.committer && commit.committer.date)
|| 1; || 1;

View File

@ -138,7 +138,7 @@ export default new Provider({
return entries.map((entry) => { return entries.map((entry) => {
const email = entry.author_email || entry.committer_email; const email = entry.author_email || entry.committer_email;
const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`; const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`;
userSvc.addInfo({ userSvc.addUserInfo({
id: sub, id: sub,
name: entry.author_name || entry.committer_name, name: entry.author_name || entry.committer_name,
imageUrl: '', imageUrl: '',

View File

@ -4,6 +4,7 @@ import Provider from './common/Provider';
import utils from '../utils'; import utils from '../utils';
import userSvc from '../userSvc'; import userSvc from '../userSvc';
import gitWorkspaceSvc from '../gitWorkspaceSvc'; import gitWorkspaceSvc from '../gitWorkspaceSvc';
import badgeSvc from '../badgeSvc';
const getAbsolutePath = ({ id }) => const getAbsolutePath = ({ id }) =>
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`; `${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
@ -98,6 +99,7 @@ export default new Provider({
}); });
} }
badgeSvc.addBadge('addGitlabWorkspace');
return store.getters['workspace/workspacesById'][workspaceId]; return store.getters['workspace/workspacesById'][workspaceId];
}, },
getChanges() { getChanges() {
@ -252,7 +254,7 @@ export default new Provider({
return entries.map((entry) => { return entries.map((entry) => {
const email = entry.author_email || entry.committer_email; const email = entry.author_email || entry.committer_email;
const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`; const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`;
userSvc.addInfo({ userSvc.addUserInfo({
id: sub, id: sub,
name: entry.author_name || entry.committer_name, name: entry.author_name || entry.committer_name,
imageUrl: '', // No way to get user's avatar url... imageUrl: '', // No way to get user's avatar url...

View File

@ -3,6 +3,7 @@ import googleHelper from './helpers/googleHelper';
import Provider from './common/Provider'; import Provider from './common/Provider';
import utils from '../utils'; import utils from '../utils';
import workspaceSvc from '../workspaceSvc'; import workspaceSvc from '../workspaceSvc';
import badgeSvc from '../badgeSvc';
let fileIdToOpen; let fileIdToOpen;
let syncStartPageToken; let syncStartPageToken;
@ -137,6 +138,7 @@ export default new Provider({
await initFolder(token, folder); await initFolder(token, folder);
} }
badgeSvc.addBadge('addGoogleDriveWorkspace');
return getWorkspace(folderId); return getWorkspace(folderId);
}, },
async performAction() { async performAction() {

View File

@ -1,6 +1,7 @@
import networkSvc from '../../networkSvc'; import networkSvc from '../../networkSvc';
import userSvc from '../../userSvc'; import userSvc from '../../userSvc';
import store from '../../../store'; import store from '../../../store';
import badgeSvc from '../../badgeSvc';
const getAppKey = (fullAccess) => { const getAppKey = (fullAccess) => {
if (fullAccess) { if (fullAccess) {
@ -73,7 +74,7 @@ export default {
method: 'POST', method: 'POST',
url: 'https://api.dropboxapi.com/2/users/get_current_account', url: 'https://api.dropboxapi.com/2/users/get_current_account',
}); });
userSvc.addInfo({ userSvc.addUserInfo({
id: `${subPrefix}:${body.account_id}`, id: `${subPrefix}:${body.account_id}`,
name: body.name.display_name, name: body.name.display_name,
imageUrl: body.profile_photo_url || '', imageUrl: body.profile_photo_url || '',
@ -96,8 +97,10 @@ export default {
store.dispatch('data/addDropboxToken', token); store.dispatch('data/addDropboxToken', token);
return token; return token;
}, },
addAccount(fullAccess = false) { async addAccount(fullAccess = false) {
return this.startOauth2(fullAccess); const token = await this.startOauth2(fullAccess);
badgeSvc.addBadge('addDropboxAccount');
return token;
}, },
/** /**

View File

@ -2,6 +2,7 @@ import utils from '../../utils';
import networkSvc from '../../networkSvc'; import networkSvc from '../../networkSvc';
import store from '../../../store'; import store from '../../../store';
import userSvc from '../../userSvc'; import userSvc from '../../userSvc';
import badgeSvc from '../../badgeSvc';
const clientId = GITHUB_CLIENT_ID; const clientId = GITHUB_CLIENT_ID;
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist']; const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
@ -90,7 +91,7 @@ export default {
access_token: accessToken, access_token: accessToken,
}, },
})).body; })).body;
userSvc.addInfo({ userSvc.addUserInfo({
id: `${subPrefix}:${user.id}`, id: `${subPrefix}:${user.id}`,
name: user.login, name: user.login,
imageUrl: user.avatar_url || '', imageUrl: user.avatar_url || '',
@ -107,7 +108,7 @@ export default {
accessToken, accessToken,
name: user.login, name: user.login,
sub: `${user.id}`, sub: `${user.id}`,
repoFullAccess: scopes.indexOf('repo') !== -1, repoFullAccess: scopes.includes('repo'),
}; };
// Add token to github tokens // Add token to github tokens
@ -115,7 +116,9 @@ export default {
return token; return token;
}, },
async addAccount(repoFullAccess = false) { async addAccount(repoFullAccess = false) {
return this.startOauth2(getScopes({ repoFullAccess })); const token = await this.startOauth2(getScopes({ repoFullAccess }));
badgeSvc.addBadge('addGitHubAccount');
return token;
}, },
/** /**

View File

@ -2,6 +2,7 @@ import utils from '../../utils';
import networkSvc from '../../networkSvc'; import networkSvc from '../../networkSvc';
import store from '../../../store'; import store from '../../../store';
import userSvc from '../../userSvc'; import userSvc from '../../userSvc';
import badgeSvc from '../../badgeSvc';
const request = ({ accessToken, serverUrl }, options) => networkSvc.request({ const request = ({ accessToken, serverUrl }, options) => networkSvc.request({
...options, ...options,
@ -65,7 +66,7 @@ export default {
url: 'user', url: 'user',
}); });
const uniqueSub = `${serverUrl}/${user.id}`; const uniqueSub = `${serverUrl}/${user.id}`;
userSvc.addInfo({ userSvc.addUserInfo({
id: `${subPrefix}:${uniqueSub}`, id: `${subPrefix}:${uniqueSub}`,
name: user.username, name: user.username,
imageUrl: user.avatar_url || '', imageUrl: user.avatar_url || '',
@ -88,8 +89,10 @@ export default {
store.dispatch('data/addGitlabToken', token); store.dispatch('data/addGitlabToken', token);
return token; return token;
}, },
addAccount(serverUrl, applicationId, sub = null) { async addAccount(serverUrl, applicationId, sub = null) {
return this.startOauth2(serverUrl, applicationId, sub); const token = await this.startOauth2(serverUrl, applicationId, sub);
badgeSvc.addBadge('addGitLabAccount');
return token;
}, },
/** /**

View File

@ -2,6 +2,7 @@ import utils from '../../utils';
import networkSvc from '../../networkSvc'; import networkSvc from '../../networkSvc';
import store from '../../../store'; import store from '../../../store';
import userSvc from '../../userSvc'; import userSvc from '../../userSvc';
import badgeSvc from '../../badgeSvc';
const clientId = GOOGLE_CLIENT_ID; const clientId = GOOGLE_CLIENT_ID;
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk'; const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
@ -39,8 +40,8 @@ if (utils.queryParams.providerId === 'googleDrive') {
* https://developers.google.com/people/api/rest/v1/people/get * https://developers.google.com/people/api/rest/v1/people/get
*/ */
const getUser = async (sub, token) => { const getUser = async (sub, token) => {
const url = `https://people.googleapis.com/v1/people/${sub}?personFields=names,photos`; const url = `https://people.googleapis.com/v1/people/${sub}?personFields=names,photos&key=${apiKey}`;
const { body } = await networkSvc.request(token const { body } = await networkSvc.request(sub === 'me' && token
? { ? {
method: 'GET', method: 'GET',
url, url,
@ -50,7 +51,7 @@ const getUser = async (sub, token) => {
} }
: { : {
method: 'GET', method: 'GET',
url: `${url}&key=${apiKey}`, url,
}, true); }, true);
return body; return body;
}; };
@ -60,8 +61,8 @@ userSvc.setInfoResolver('google', subPrefix, async (sub) => {
try { try {
const googleToken = Object.values(store.getters['data/googleTokensBySub'])[0]; const googleToken = Object.values(store.getters['data/googleTokensBySub'])[0];
const body = await getUser(sub, googleToken); const body = await getUser(sub, googleToken);
const name = body.names[0] || {}; const name = (body.names && body.names[0]) || {};
const photo = body.photos[0] || {}; const photo = (body.photos && body.photos[0]) || {};
return { return {
id: `${subPrefix}:${sub}`, id: `${subPrefix}:${sub}`,
name: name.displayName, name: name.displayName,
@ -150,15 +151,15 @@ export default {
expiresOn: Date.now() + (expiresIn * 1000), expiresOn: Date.now() + (expiresIn * 1000),
idToken, idToken,
sub: body.sub, sub: body.sub,
name: (existingToken || {}).name || 'Unknown', name: (existingToken || {}).name || 'Someone',
isLogin: !store.getters['workspace/mainWorkspaceToken'] && isLogin: !store.getters['workspace/mainWorkspaceToken'] &&
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1, scopes.includes('https://www.googleapis.com/auth/drive.appdata'),
isSponsor: false, isSponsor: false,
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 || isDrive: scopes.includes('https://www.googleapis.com/auth/drive') ||
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1, scopes.includes('https://www.googleapis.com/auth/drive.file'),
isBlogger: scopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1, isBlogger: scopes.includes('https://www.googleapis.com/auth/blogger'),
isPhotos: scopes.indexOf('https://www.googleapis.com/auth/photos') !== -1, isPhotos: scopes.includes('https://www.googleapis.com/auth/photos'),
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1, driveFullAccess: scopes.includes('https://www.googleapis.com/auth/drive'),
}; };
// Call the user info endpoint // Call the user info endpoint
@ -169,7 +170,7 @@ export default {
if (name.displayName) { if (name.displayName) {
token.name = name.displayName; token.name = name.displayName;
} }
userSvc.addInfo({ userSvc.addUserInfo({
id: `${subPrefix}:${userId}`, id: `${subPrefix}:${userId}`,
name: name.displayName, name: name.displayName,
imageUrl: (photo.url || '').replace(/\bsz?=\d+$/, 'sz=40'), imageUrl: (photo.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
@ -191,13 +192,17 @@ export default {
if (token.isLogin) { if (token.isLogin) {
try { try {
token.isSponsor = (await networkSvc.request({ const res = await networkSvc.request({
method: 'GET', method: 'GET',
url: 'userInfo', url: 'userInfo',
params: { params: {
idToken: token.idToken, idToken: token.idToken,
}, },
})).body.sponsorUntil > Date.now(); });
token.isSponsor = res.body.sponsorUntil > Date.now();
if (token.isSponsor) {
badgeSvc.addBadge('sponsor');
}
} catch (err) { } catch (err) {
// Ignore // Ignore
} }
@ -245,14 +250,20 @@ export default {
signin() { signin() {
return this.startOauth2(driveAppDataScopes); return this.startOauth2(driveAppDataScopes);
}, },
addDriveAccount(fullAccess = false, sub = null) { async addDriveAccount(fullAccess = false, sub = null) {
return this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }), sub); const token = await this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }), sub);
badgeSvc.addBadge('addGoogleDriveAccount');
return token;
}, },
addBloggerAccount() { async addBloggerAccount() {
return this.startOauth2(bloggerScopes); const token = await this.startOauth2(bloggerScopes);
badgeSvc.addBadge('addBloggerAccount');
return token;
}, },
addPhotosAccount() { async addPhotosAccount() {
return this.startOauth2(photosScopes); const token = await this.startOauth2(photosScopes);
badgeSvc.addBadge('addGooglePhotosAccount');
return token;
}, },
async getSponsorship(token) { async getSponsorship(token) {
const refreshedToken = await this.refreshToken(token); const refreshedToken = await this.refreshToken(token);
@ -296,10 +307,10 @@ export default {
options.url = `https://www.googleapis.com/drive/v3/files/${fileId}`; options.url = `https://www.googleapis.com/drive/v3/files/${fileId}`;
if (parents && oldParents) { if (parents && oldParents) {
params.addParents = parents params.addParents = parents
.filter(parent => oldParents.indexOf(parent) === -1) .filter(parent => !oldParents.includes(parent))
.join(','); .join(',');
params.removeParents = oldParents params.removeParents = oldParents
.filter(parent => parents.indexOf(parent) === -1) .filter(parent => !parents.includes(parent))
.join(','); .join(',');
} }
} else if (parents) { } else if (parents) {
@ -457,7 +468,7 @@ export default {
}, },
}); });
revisions.forEach((revision) => { revisions.forEach((revision) => {
userSvc.addInfo({ userSvc.addUserInfo({
id: `${subPrefix}:${revision.lastModifyingUser.permissionId}`, id: `${subPrefix}:${revision.lastModifyingUser.permissionId}`,
name: revision.lastModifyingUser.displayName, name: revision.lastModifyingUser.displayName,
imageUrl: revision.lastModifyingUser.photoLink || '', imageUrl: revision.lastModifyingUser.photoLink || '',

View File

@ -1,5 +1,6 @@
import networkSvc from '../../networkSvc'; import networkSvc from '../../networkSvc';
import store from '../../../store'; import store from '../../../store';
import badgeSvc from '../../badgeSvc';
const clientId = '23361'; const clientId = '23361';
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (WordPress tokens expire after 2 weeks) const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (WordPress tokens expire after 2 weeks)
@ -63,8 +64,10 @@ export default {
}); });
return this.startOauth2(sub); return this.startOauth2(sub);
}, },
addAccount(fullAccess = false) { async addAccount(fullAccess = false) {
return this.startOauth2(fullAccess); const token = await this.startOauth2(fullAccess);
badgeSvc.addBadge('addWordpressAccount');
return token;
}, },
/** /**

View File

@ -1,5 +1,6 @@
import networkSvc from '../../networkSvc'; import networkSvc from '../../networkSvc';
import store from '../../../store'; import store from '../../../store';
import badgeSvc from '../../badgeSvc';
const request = (token, options) => networkSvc.request({ const request = (token, options) => networkSvc.request({
...options, ...options,
@ -49,8 +50,10 @@ export default {
store.dispatch('data/addZendeskToken', token); store.dispatch('data/addZendeskToken', token);
return token; return token;
}, },
addAccount(subdomain, clientId) { async addAccount(subdomain, clientId) {
return this.startOauth2(subdomain, clientId); const token = await this.startOauth2(subdomain, clientId);
badgeSvc.addBadge('addZendeskAccount');
return token;
}, },
/** /**

View File

@ -5,6 +5,7 @@ import networkSvc from './networkSvc';
import exportSvc from './exportSvc'; import exportSvc from './exportSvc';
import providerRegistry from './providers/common/providerRegistry'; import providerRegistry from './providers/common/providerRegistry';
import workspaceSvc from './workspaceSvc'; import workspaceSvc from './workspaceSvc';
import badgeSvc from './badgeSvc';
const hasCurrentFilePublishLocations = () => !!store.getters['publishLocation/current'].length; const hasCurrentFilePublishLocations = () => !!store.getters['publishLocation/current'].length;
@ -112,10 +113,11 @@ const requestPublish = () => {
if (networkSvc.isUserActive()) { if (networkSvc.isUserActive()) {
clearInterval(intervalId); clearInterval(intervalId);
if (!hasCurrentFilePublishLocations()) { if (!hasCurrentFilePublishLocations()) {
// Cancel sync // Cancel publish
throw new Error('Publish not possible.'); throw new Error('Publish not possible.');
} }
await publishFile(store.getters['file/current'].id); await publishFile(store.getters['file/current'].id);
badgeSvc.addBadge('triggerPublish');
} }
}; };
intervalId = utils.setInterval(() => attempt(), 1000); intervalId = utils.setInterval(() => attempt(), 1000);
@ -123,7 +125,7 @@ const requestPublish = () => {
}); });
}; };
const createPublishLocation = (publishLocation) => { const createPublishLocation = (publishLocation, featureId) => {
const currentFile = store.getters['file/current']; const currentFile = store.getters['file/current'];
publishLocation.fileId = currentFile.id; publishLocation.fileId = currentFile.id;
store.dispatch( store.dispatch(
@ -132,6 +134,9 @@ const createPublishLocation = (publishLocation) => {
const publishLocationToStore = await publish(publishLocation); const publishLocationToStore = await publish(publishLocation);
workspaceSvc.addPublishLocation(publishLocationToStore); workspaceSvc.addPublishLocation(publishLocationToStore);
store.dispatch('notification/info', `A new publication location was added to "${currentFile.name}".`); store.dispatch('notification/info', `A new publication location was added to "${currentFile.name}".`);
if (featureId) {
badgeSvc.addBadge(featureId);
}
}, },
); );
}; };

View File

@ -1,55 +0,0 @@
import store from '../store';
import networkSvc from './networkSvc';
import utils from './utils';
const checkPaymentEvery = 15 * 60 * 1000; // 15 min
let lastCheck = 0;
const appId = 'ESTHdCYOi18iLhhO';
let monetize;
const getMonetize = async () => {
await networkSvc.loadScript('https://cdn.monetizejs.com/api/js/latest/monetize.min.js');
monetize = monetize || new window.MonetizeJS({
applicationID: appId,
});
};
const isGoogleSponsor = () => {
const sponsorToken = store.getters['workspace/sponsorToken'];
return sponsorToken && sponsorToken.isSponsor;
};
const checkPayment = async () => {
const currentDate = Date.now();
if (!isGoogleSponsor()
&& networkSvc.isUserActive()
&& !store.state.offline
&& !store.state.light
&& lastCheck + checkPaymentEvery < currentDate
) {
lastCheck = currentDate;
await getMonetize();
monetize.getPaymentsImmediate((err, payments) => {
const isSponsor = payments && payments.app === appId && (
(payments.chargeOption && payments.chargeOption.alias === 'once') ||
(payments.subscriptionOption && payments.subscriptionOption.alias === 'yearly'));
if (isSponsor !== store.state.monetizeSponsor) {
store.commit('setMonetizeSponsor', isSponsor);
}
});
}
};
export default {
init: () => {
utils.setInterval(checkPayment, 2000);
},
async getToken() {
if (isGoogleSponsor() || store.state.offline) {
return null;
}
await getMonetize();
return new Promise(resolve => monetize.getTokenImmediate((err, result) => resolve(result)));
},
};

View File

@ -12,6 +12,7 @@ import './providers/googleDriveWorkspaceProvider';
import tempFileSvc from './tempFileSvc'; import tempFileSvc from './tempFileSvc';
import workspaceSvc from './workspaceSvc'; import workspaceSvc from './workspaceSvc';
import constants from '../data/constants'; import constants from '../data/constants';
import badgeSvc from './badgeSvc';
const minAutoSyncEvery = 60 * 1000; // 60 sec const minAutoSyncEvery = 60 * 1000; // 60 sec
const inactivityThreshold = 3 * 1000; // 3 sec const inactivityThreshold = 3 * 1000; // 3 sec
@ -455,7 +456,7 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem; newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
if (serverContent && if (serverContent &&
(serverContent.hash === newSyncHistoryItem[LAST_SEEN] || (serverContent.hash === newSyncHistoryItem[LAST_SEEN] ||
serverContent.history.indexOf(newSyncHistoryItem[LAST_SEEN]) !== -1) serverContent.history.includes(newSyncHistoryItem[LAST_SEEN]))
) { ) {
// That's the 2nd time we've seen this content, trust it for future merges // That's the 2nd time we've seen this content, trust it for future merges
newSyncHistoryItem[LAST_MERGED] = newSyncHistoryItem[LAST_SEEN]; newSyncHistoryItem[LAST_MERGED] = newSyncHistoryItem[LAST_SEEN];
@ -723,10 +724,11 @@ const syncWorkspace = async (skipContents = false) => {
return true; return true;
})); }));
// Sync settings and workspaces only in the main workspace // Sync settings, workspaces and badges only in the main workspace
if (workspace.id === 'main') { if (workspace.id === 'main') {
await syncDataItem('settings'); await syncDataItem('settings');
await syncDataItem('workspaces'); await syncDataItem('workspaces');
await syncDataItem('badges');
} }
await syncDataItem('templates'); await syncDataItem('templates');
@ -786,6 +788,10 @@ const syncWorkspace = async (skipContents = false) => {
if (syncContext.restartSkipContents) { if (syncContext.restartSkipContents) {
await syncWorkspace(true); await syncWorkspace(true);
} }
if (workspace.id === 'main') {
badgeSvc.addBadge('syncMainWorkspace');
}
} catch (err) { } catch (err) {
if (err && err.message === 'TOO_LATE') { if (err && err.message === 'TOO_LATE') {
// Restart sync // Restart sync
@ -799,7 +805,7 @@ const syncWorkspace = async (skipContents = false) => {
/** /**
* Enqueue a sync task, if possible. * Enqueue a sync task, if possible.
*/ */
const requestSync = () => { const requestSync = (addTriggerSyncBadge = false) => {
// No sync in light mode // No sync in light mode
if (store.state.light) { if (store.state.light) {
return; return;
@ -851,6 +857,10 @@ const requestSync = () => {
workspaceSvc.deleteFile(fileId); workspaceSvc.deleteFile(fileId);
} }
}); });
if (addTriggerSyncBadge) {
badgeSvc.addBadge('triggerSync');
}
} finally { } finally {
clearInterval(intervalId); clearInterval(intervalId);
} }

View File

@ -1,16 +1,47 @@
import store from '../store'; import store from '../store';
import utils from './utils';
const refreshUserInfoAfter = 60 * 60 * 1000; // 60 minutes
const infoPromisesByUserId = {};
const infoResolversByType = {}; const infoResolversByType = {};
const subPrefixesByType = {}; const subPrefixesByType = {};
const typesBySubPrefix = {}; const typesBySubPrefix = {};
const parseUserId = (userId) => { const lastInfosByUserId = {};
const infoPromisedByUserId = {};
const sanitizeUserId = (userId) => {
const prefix = userId[2] === ':' && userId.slice(0, 2); const prefix = userId[2] === ':' && userId.slice(0, 2);
const type = typesBySubPrefix[prefix]; if (typesBySubPrefix[prefix]) {
return type ? [type, userId.slice(3)] : ['google', userId]; return userId;
}
return `go:${userId}`;
}; };
const parseUserId = userId => [typesBySubPrefix[userId.slice(0, 2)], userId.slice(3)];
const refreshUserInfos = () => {
if (store.state.offline) {
return;
}
Object.entries(lastInfosByUserId)
.filter(([userId, lastInfo]) => lastInfo === 0 && !infoPromisedByUserId[userId])
.forEach(async ([userId]) => {
const [type, sub] = parseUserId(userId);
const infoResolver = infoResolversByType[type];
if (infoResolver) {
try {
infoPromisedByUserId[userId] = true;
const userInfo = await infoResolver(sub);
store.commit('userInfo/setItem', userInfo);
} finally {
infoPromisedByUserId[userId] = false;
lastInfosByUserId[userId] = Date.now();
}
}
});
};
export default { export default {
setInfoResolver(type, subPrefix, resolver) { setInfoResolver(type, subPrefix, resolver) {
@ -27,53 +58,34 @@ export default {
const prefix = subPrefixesByType[loginType]; const prefix = subPrefixesByType[loginType];
return prefix ? `${prefix}:${loginToken.sub}` : loginToken.sub; return prefix ? `${prefix}:${loginToken.sub}` : loginToken.sub;
}, },
addInfo(info) { sanitizeUserId,
infoPromisesByUserId[info.id] = Promise.resolve(info); addUserInfo(userInfo) {
store.commit('userInfo/addItem', info); store.commit('userInfo/setItem', userInfo);
lastInfosByUserId[userInfo.id] = Date.now();
}, },
async getInfo(userId) { addUserId(userId) {
if (!userId) { if (userId) {
return {}; const sanitizedUserId = sanitizeUserId(userId);
} const lastInfo = lastInfosByUserId[sanitizedUserId];
if (lastInfo === undefined) {
let infoPromise = infoPromisesByUserId[userId];
if (infoPromise) {
return infoPromise;
}
const [type, sub] = parseUserId(userId);
// Try to find a token with this sub to resolve name as soon as possible // Try to find a token with this sub to resolve name as soon as possible
const [type, sub] = parseUserId(sanitizedUserId);
const token = store.getters['data/tokensByType'][type][sub]; const token = store.getters['data/tokensByType'][type][sub];
if (token) { if (token) {
store.commit('userInfo/addItem', { store.commit('userInfo/setItem', {
id: userId, id: sanitizedUserId,
name: token.name, name: token.name,
}); });
} }
if (store.state.offline) {
return {};
} }
// Get user info from helper if (lastInfo === undefined || lastInfo + refreshUserInfoAfter < Date.now()) {
infoPromise = new Promise(async (resolve) => { lastInfosByUserId[sanitizedUserId] = 0;
const infoResolver = infoResolversByType[type]; refreshUserInfos();
if (infoResolver) {
try {
const userInfo = await infoResolver(sub);
this.addInfo(userInfo);
resolve(userInfo);
} catch (err) {
if (err && err.message === 'RETRY') {
infoPromisesByUserId[userId] = null;
}
resolve({});
} }
} }
});
infoPromisesByUserId[userId] = infoPromise;
return infoPromise;
}, },
}; };
// Get user info periodically
utils.setInterval(() => refreshUserInfos(), 60 * 1000);

View File

@ -1,6 +1,7 @@
import store from '../store'; import store from '../store';
import utils from './utils'; import utils from './utils';
import constants from '../data/constants'; import constants from '../data/constants';
import badgeSvc from './badgeSvc';
const forbiddenFolderNameMatcher = /^\.stackedit-data$|^\.stackedit-trash$|\.md$|\.sync$|\.publish$/; const forbiddenFolderNameMatcher = /^\.stackedit-data$|^\.stackedit-trash$|\.md$|\.sync$|\.publish$/;
@ -249,8 +250,13 @@ export default {
...location, ...location,
id: utils.uid(), id: utils.uid(),
}); });
// Sanitize the workspace // Sanitize the workspace
this.ensureUniqueLocations(); this.ensureUniqueLocations();
if (Object.keys(store.getters['syncLocation/currentWithWorkspaceSyncLocation']).length > 1) {
badgeSvc.addBadge('syncMultipleLocations');
}
}, },
addPublishLocation(location) { addPublishLocation(location) {
@ -258,8 +264,13 @@ export default {
...location, ...location,
id: utils.uid(), id: utils.uid(),
}); });
// Sanitize the workspace // Sanitize the workspace
this.ensureUniqueLocations(); this.ensureUniqueLocations();
if (Object.keys(store.getters['publishLocation/current']).length > 1) {
badgeSvc.addBadge('publishMultipleLocations');
}
}, },
/** /**

View File

@ -3,6 +3,7 @@ import moduleTemplate from './moduleTemplate';
import empty from '../data/empties/emptyContent'; import empty from '../data/empties/emptyContent';
import utils from '../services/utils'; import utils from '../services/utils';
import cledit from '../services/editor/cledit'; import cledit from '../services/editor/cledit';
import badgeSvc from '../services/badgeSvc';
const diffMatchPatch = new DiffMatchPatch(); const diffMatchPatch = new DiffMatchPatch();
@ -104,6 +105,7 @@ module.actions = {
...currentContent, ...currentContent,
text: revisionContent.originalText, text: revisionContent.originalText,
}); });
badgeSvc.addBadge('restoreVersion');
} }
} }
}, },

View File

@ -10,6 +10,7 @@ import styledHtmlTemplate from '../data/templates/styledHtmlTemplate.html';
import styledHtmlWithTocTemplate from '../data/templates/styledHtmlWithTocTemplate.html'; import styledHtmlWithTocTemplate from '../data/templates/styledHtmlWithTocTemplate.html';
import jekyllSiteTemplate from '../data/templates/jekyllSiteTemplate.html'; import jekyllSiteTemplate from '../data/templates/jekyllSiteTemplate.html';
import constants from '../data/constants'; import constants from '../data/constants';
import features from '../data/features';
const itemTemplate = (id, data = {}) => ({ const itemTemplate = (id, data = {}) => ({
id, id,
@ -203,7 +204,19 @@ export default {
gitlabTokensBySub: (state, { tokensByType }) => tokensByType.gitlab || {}, gitlabTokensBySub: (state, { tokensByType }) => tokensByType.gitlab || {},
wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {}, wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {},
zendeskTokensBySub: (state, { tokensByType }) => tokensByType.zendesk || {}, zendeskTokensBySub: (state, { tokensByType }) => tokensByType.zendesk || {},
badgesByFeatureId: getter('badges'), badges: getter('badges'),
badgeTree: (state, { badges }) => features.map(feature => feature.toBadge(badges)),
allBadges: (state, { badgeTree }) => {
const result = [];
const processBadgeNodes = nodes => nodes.forEach((node) => {
result.push(node);
if (node.children) {
processBadgeNodes(node.children);
}
});
processBadgeNodes(badgeTree);
return result;
},
}, },
actions: { actions: {
setSettings: setter('settings'), setSettings: setter('settings'),
@ -275,5 +288,6 @@ export default {
addGitlabToken: tokenAdder('gitlab'), addGitlabToken: tokenAdder('gitlab'),
addWordpressToken: tokenAdder('wordpress'), addWordpressToken: tokenAdder('wordpress'),
addZendeskToken: tokenAdder('zendesk'), addZendeskToken: tokenAdder('zendesk'),
patchBadges: patcher('badges'),
}, },
}; };

View File

@ -3,7 +3,8 @@ import googleHelper from '../services/providers/helpers/googleHelper';
import syncSvc from '../services/syncSvc'; import syncSvc from '../services/syncSvc';
const idShifter = offset => (state, getters) => { const idShifter = offset => (state, getters) => {
const ids = Object.keys(getters.currentFileDiscussions); const ids = Object.keys(getters.currentFileDiscussions)
.filter(id => id !== state.newDiscussionId);
const idx = ids.indexOf(state.currentDiscussionId) + offset + ids.length; const idx = ids.indexOf(state.currentDiscussionId) + offset + ids.length;
return ids[idx % ids.length]; return ids[idx % ids.length];
}; };

View File

@ -52,8 +52,7 @@ const store = new Vuex.Store({
light: false, light: false,
offline: false, offline: false,
lastOfflineCheck: 0, lastOfflineCheck: 0,
minuteCounter: 0, timeCounter: 0,
monetizeSponsor: false,
}, },
mutations: { mutations: {
setLight: (state, value) => { setLight: (state, value) => {
@ -65,14 +64,8 @@ const store = new Vuex.Store({
updateLastOfflineCheck: (state) => { updateLastOfflineCheck: (state) => {
state.lastOfflineCheck = Date.now(); state.lastOfflineCheck = Date.now();
}, },
updateMinuteCounter: (state) => { updateTimeCounter: (state) => {
state.minuteCounter += 1; state.timeCounter += 1;
},
setMonetizeSponsor: (state, value) => {
state.monetizeSponsor = value;
},
setGoogleSponsor: (state, value) => {
state.googleSponsor = value;
}, },
}, },
getters: { getters: {
@ -161,9 +154,9 @@ const store = new Vuex.Store({
}); });
return result; return result;
}, },
isSponsor: ({ light, monetizeSponsor }, getters) => { isSponsor: ({ light }, getters) => {
const sponsorToken = getters['workspace/sponsorToken']; const sponsorToken = getters['workspace/sponsorToken'];
return light || monetizeSponsor || (sponsorToken && sponsorToken.isSponsor); return light || (sponsorToken && sponsorToken.isSponsor);
}, },
}, },
actions: { actions: {
@ -183,7 +176,7 @@ const store = new Vuex.Store({
}); });
setInterval(() => { setInterval(() => {
store.commit('updateMinuteCounter'); store.commit('updateTimeCounter');
}, 60 * 1000); }, 30 * 1000);
export default store; export default store;

View File

@ -10,7 +10,7 @@ pagedownButtons.forEach((button) => {
} }
}); });
const minPadding = 20; const minPadding = 25;
const editorTopPadding = 10; const editorTopPadding = 10;
const navigationBarEditButtonsWidth = (34 * buttonCount) + (8 * spacerCount); // buttons + spacers const navigationBarEditButtonsWidth = (34 * buttonCount) + (8 * spacerCount); // buttons + spacers
const navigationBarLeftButtonWidth = 38 + 4 + 12; const navigationBarLeftButtonWidth = 38 + 4 + 12;

View File

@ -45,16 +45,22 @@ export default {
return item.promise; return item.promise;
}, },
info({ dispatch }, info) { info({ dispatch }, content) {
return dispatch('showItem', { return dispatch('showItem', {
type: 'info', type: 'info',
content: info, content,
}); });
}, },
confirm({ dispatch }, question) { badge({ dispatch }, content) {
return dispatch('showItem', {
type: 'badge',
content,
});
},
confirm({ dispatch }, content) {
return dispatch('showItem', { return dispatch('showItem', {
type: 'confirm', type: 'confirm',
content: question, content,
timeout: 10000, // 10 sec timeout: 10000, // 10 sec
}); });
}, },

View File

@ -6,8 +6,20 @@ export default {
itemsById: {}, itemsById: {},
}, },
mutations: { mutations: {
addItem: ({ itemsById }, item) => { setItem: ({ itemsById }, item) => {
Vue.set(itemsById, item.id, item); const itemToSet = {
...item,
};
const existingItem = itemsById[item.id];
if (existingItem) {
if (!itemToSet.name) {
itemToSet.name = existingItem.name;
}
if (!itemToSet.imageUrl) {
itemToSet.imageUrl = existingItem.imageUrl;
}
}
Vue.set(itemsById, item.id, itemToSet);
}, },
}, },
}; };

View File

@ -273,10 +273,6 @@ textarea {
position: absolute; position: absolute;
top: 0; top: 0;
height: 100%; height: 100%;
& > * {
border-left: 2px solid transparent;
}
} }
.gutter__background { .gutter__background {
@ -289,8 +285,8 @@ textarea {
color: rgba(0, 0, 0, 0.33); color: rgba(0, 0, 0, 0.33);
position: absolute; position: absolute;
left: 0; left: 0;
padding: 2px 3px 2px 0; padding: 3px 3px 3px 0;
width: 20px; width: 22px;
height: 21px; height: 21px;
line-height: 1; line-height: 1;