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)
? req.query.format
: 'pdf';
Promise.all([
user.checkSponsor(req.query.idToken),
user.checkMonetize(req.query.token),
])
.then(([isSponsor, isMonetize]) => {
if (!isSponsor && !isMonetize) {
user.checkSponsor(req.query.idToken)
.then((isSponsor) => {
if (!isSponsor) {
throw new Error('unauthorized');
}
@ -79,7 +76,7 @@ exports.generate = (req, res) => {
if (!Number.isNaN(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);
Object.keys(metadata).forEach((key) => {
params.push('-M', `${key}=${metadata[key]}`);

View File

@ -50,12 +50,9 @@ const readJson = (str) => {
exports.generate = (req, res) => {
let wkhtmltopdfError = '';
Promise.all([
user.checkSponsor(req.query.idToken),
user.checkMonetize(req.query.token),
])
.then(([isSponsor, isMonetize]) => {
if (!isSponsor && !isMonetize) {
user.checkSponsor(req.query.idToken)
.then((isSponsor) => {
if (!isSponsor) {
throw new Error('unauthorized');
}
return new Promise((resolve, reject) => {
@ -127,7 +124,7 @@ exports.generate = (req, res) => {
}
// 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
const binPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';

View File

@ -116,21 +116,3 @@ exports.checkSponsor = (idToken) => {
return exports.getUserFromToken(idToken)
.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 syncSvc from '../services/syncSvc';
import networkSvc from '../services/networkSvc';
import sponsorSvc from '../services/sponsorSvc';
import tempFileSvc from '../services/tempFileSvc';
import store from '../store';
import './common/vueGlobals';
@ -55,7 +54,6 @@ export default {
try {
await syncSvc.init();
await networkSvc.init();
await sponsorSvc.init();
this.ready = true;
tempFileSvc.setReady();
} catch (err) {

View File

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

View File

@ -177,11 +177,9 @@ export default {
.sticky-comment,
.current-discussion {
background-color: mix(#000, $editor-background-light, 6.7%);
border-color: $editor-background-light;
.app--dark & {
background-color: mix(#fff, $editor-background-dark, 6.7%);
border-color: $editor-background-dark;
}
}
}
@ -204,7 +202,6 @@ $preview-background-dark: #252525;
.sticky-comment,
.current-discussion {
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 WorkspaceManagementModal from './modals/WorkspaceManagementModal';
import AccountManagementModal from './modals/AccountManagementModal';
import BadgeManagementModal from './modals/BadgeManagementModal';
import SponsorModal from './modals/SponsorModal';
// Providers
@ -88,6 +89,7 @@ export default {
PublishManagementModal,
WorkspaceManagementModal,
AccountManagementModal,
BadgeManagementModal,
SponsorModal,
// Providers
GooglePhotoModal,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -56,6 +56,7 @@ import utils from '../../services/utils';
import googleHelper from '../../services/providers/helpers/googleHelper';
import syncSvc from '../../services/syncSvc';
import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
let editorClassAppliers = [];
let previewClassAppliers = [];
@ -237,10 +238,12 @@ export default {
syncLocation: {
immediate: true,
handler(value) {
if (!value) {
const firstSyncLocation = this.syncLocations[0];
if (firstSyncLocation) {
const firstSyncLocation = this.syncLocations[0];
if (firstSyncLocation) {
if (!value) {
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 workspaceSvc from '../../services/workspaceSvc';
import exportSvc from '../../services/exportSvc';
import badgeSvc from '../../services/badgeSvc';
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
@ -85,6 +86,7 @@ export default {
name: file.name,
});
store.commit('file/setCurrentId', item.id);
badgeSvc.addBadge('importMarkdown');
},
async onImportHtml(evt) {
const file = evt.target.files[0];
@ -96,23 +98,29 @@ export default {
name: file.name,
});
store.commit('file/setCurrentId', item.id);
badgeSvc.addBadge('importHtml');
},
exportMarkdown() {
async exportMarkdown() {
const currentFile = store.getters['file/current'];
return exportSvc.exportToDisk(currentFile.id, 'md')
.catch(() => { /* Cancel */ });
try {
await exportSvc.exportToDisk(currentFile.id, 'md');
badgeSvc.addBadge('exportMarkdown');
} catch (e) { /* Cancel */ }
},
exportHtml() {
return store.dispatch('modal/open', 'htmlExport')
.catch(() => { /* Cancel */ });
async exportHtml() {
try {
await store.dispatch('modal/open', 'htmlExport');
} catch (e) { /* Cancel */ }
},
exportPdf() {
return store.dispatch('modal/open', 'pdfExport')
.catch(() => { /* Cancel */ });
async exportPdf() {
try {
await store.dispatch('modal/open', 'pdfExport');
} catch (e) { /* Cancel */ }
},
exportPandoc() {
return store.dispatch('modal/open', 'pandocExport')
.catch(() => { /* Cancel */ });
async exportPandoc() {
try {
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>
<span>Manage access to your external accounts.</span>
</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>
<menu-entry @click.native="setPanel('workspaceBackup')">
<menu-entry @click.native="setPanel('workspaceBackups')">
<icon-content-save slot="icon"></icon-content-save>
Workspace backup
Workspace backups
</menu-entry>
<menu-entry @click.native="reset">
<icon-logout slot="icon"></icon-logout>
<div>Reset application</div>
<span>Sign out and clean all workspaces.</span>
<span>Sign out and clean all workspace data.</span>
</menu-entry>
<hr>
<menu-entry @click.native="about">
@ -161,6 +166,12 @@ export default {
return Object.values(store.getters['data/tokensByType'])
.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: {
...mapActions('data', {
@ -186,35 +197,30 @@ export default {
},
async settings() {
try {
const settings = await store.dispatch('modal/open', 'settings');
store.dispatch('data/setSettings', settings);
} catch (e) {
// Cancel
}
await store.dispatch('modal/open', 'settings');
} catch (e) { /* Cancel */ }
},
async templates() {
try {
const { templates } = await store.dispatch('modal/open', 'templates');
store.dispatch('data/setTemplatesById', templates);
} catch (e) {
// Cancel
}
await store.dispatch('modal/open', 'templates');
} catch (e) { /* Cancel */ }
},
async accounts() {
try {
await store.dispatch('modal/open', 'accountManagement');
} catch (e) {
// Cancel
}
} catch (e) { /* Cancel */ }
},
async badges() {
try {
await store.dispatch('modal/open', 'badgeManagement');
} catch (e) { /* Cancel */ }
},
async reset() {
try {
await store.dispatch('modal/open', 'reset');
window.location.href = '#reset=true';
window.location.reload();
} catch (e) {
// Cancel
}
} catch (e) { /* Cancel */ }
},
about() {
store.dispatch('modal/open', 'about');

View File

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

View File

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

View File

@ -2,8 +2,6 @@
<modal-inner class="modal__inner-1--about-modal" aria-label="About">
<div class="modal__content">
<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>
<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>
@ -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>
<br>
StackEdit on <a target="_blank" href="https://twitter.com/stackedit/">Twitter</a>
<div class="modal__info">
For commercial support or custom development, please <a href="mailto:stackedit.project@gmail.com">send us an email</a>.
</div>
<hr>
<small>© 2013-2019 Dock5 Software Ltd.<br>v{{version}}</small>
<h3>FAQ</h3>
<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
<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>

View File

@ -88,6 +88,7 @@ import githubHelper from '../../services/providers/helpers/githubHelper';
import gitlabHelper from '../../services/providers/helpers/gitlabHelper';
import wordpressHelper from '../../services/providers/helpers/wordpressHelper';
import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
import badgeSvc from '../../services/badgeSvc';
export default {
components: {
@ -160,6 +161,7 @@ export default {
await store.dispatch('data/patchTokensByType', {
[entry.providerId]: tokensBySub,
});
badgeSvc.addBadge('removeAccount');
},
async addBloggerAccount() {
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 presets from '../../data/presets';
import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
const simpleProperties = {
const metadataProperties = {
title: '',
author: '',
tags: '',
@ -117,7 +118,7 @@ export default {
yamlProperties: null,
preset: '',
error: null,
...simpleProperties,
...metadataProperties,
}),
computed: {
...mapGetters('modal', [
@ -148,10 +149,10 @@ export default {
const properties = this.properties || {};
const extensions = properties.extensions || {};
this.preset = extensions.preset;
if (this.presets.indexOf(this.preset) === -1) {
if (!this.presets.includes(this.preset)) {
this.preset = 'default';
}
Object.keys(simpleProperties).forEach((name) => {
Object.keys(metadataProperties).forEach((name) => {
this[name] = `${properties[name] || ''}`;
});
},
@ -168,7 +169,7 @@ export default {
hasChanged = true;
}
}
Object.keys(simpleProperties).forEach((name) => {
Object.keys(metadataProperties).forEach((name) => {
if (this[name] !== properties[name]) {
if (this[name]) {
properties[name] = this[name];
@ -215,6 +216,17 @@ export default {
if (this.error) {
this.setYamlTab();
} 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', {
id: this.contentId,
properties: utils.sanitizeText(this.yamlProperties),

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@
</div>
<div class="modal__button-bar">
<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>
</modal-inner>
</template>
@ -38,6 +38,7 @@ import Tab from './common/Tab';
import CodeEditor from '../CodeEditor';
import defaultSettings from '../../data/defaults/defaultSettings.yml';
import store from '../../store';
import badgeSvc from '../../services/badgeSvc';
const emptySettings = `# Add your custom settings here to override the
# default settings.
@ -77,6 +78,25 @@ export default {
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>

View File

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

View File

@ -53,6 +53,7 @@
<script>
import { mapGetters } from 'vuex';
import utils from '../../services/utils';
import badgeSvc from '../../services/badgeSvc';
import ModalInner from './common/ModalInner';
import CodeEditor from '../CodeEditor';
import emptyTemplateValue from '../../data/empties/emptyTemplateValue.html';
@ -153,7 +154,22 @@ export default {
this.isEditing = false;
}, 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({
templates: this.templates,
selectedId: this.selectedId,

View File

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

View File

@ -64,11 +64,10 @@ export default (desc) => {
};
// Make use of `function` to have `this` bound to the component
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',
selectedId: this.selectedTemplate,
});
store.dispatch('data/setTemplatesById', templates);
store.dispatch('data/patchLocalSettings', {
[id]: selectedId,
});

View File

@ -19,6 +19,7 @@ export default {
'settings',
'layoutSettings',
'tokens',
'badges',
],
textMaxLength: 250000,
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 Key from './Key';
import DotsHorizontal from './DotsHorizontal';
import Seal from './Seal';
Vue.component('iconProvider', Provider);
Vue.component('iconFormatBold', FormatBold);
@ -108,3 +109,4 @@ Vue.component('iconCheckCircle', CheckCircle);
Vue.component('iconContentCopy', ContentCopy);
Vue.component('iconKey', Key);
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 workspaceSvc from './workspaceSvc';
import badgeSvc from './badgeSvc';
export default {
newItem(isFolder = false) {
@ -61,6 +62,7 @@ export default {
} else {
workspaceSvc.deleteFile(id);
}
badgeSvc.addBadge('removeFiles');
};
if (selectedNode === store.getters['explorer/selectedNode']) {

View File

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

View File

@ -66,7 +66,7 @@ export default new Provider({
return entries.map((entry) => {
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 {
sub,
id: entry.version,

View File

@ -135,7 +135,7 @@ export default new Provider({
user = committer;
}
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)
|| (commit.committer && commit.committer.date);
return {

View File

@ -4,6 +4,7 @@ import Provider from './common/Provider';
import utils from '../utils';
import userSvc from '../userSvc';
import gitWorkspaceSvc from '../gitWorkspaceSvc';
import badgeSvc from '../badgeSvc';
const getAbsolutePath = ({ id }) =>
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
@ -86,6 +87,7 @@ export default new Provider({
});
}
badgeSvc.addBadge('addGithubWorkspace');
return store.getters['workspace/workspacesById'][workspaceId];
},
getChanges() {
@ -247,7 +249,7 @@ export default new Provider({
user = committer;
}
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)
|| (commit.committer && commit.committer.date)
|| 1;

View File

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

View File

@ -4,6 +4,7 @@ import Provider from './common/Provider';
import utils from '../utils';
import userSvc from '../userSvc';
import gitWorkspaceSvc from '../gitWorkspaceSvc';
import badgeSvc from '../badgeSvc';
const getAbsolutePath = ({ id }) =>
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
@ -98,6 +99,7 @@ export default new Provider({
});
}
badgeSvc.addBadge('addGitlabWorkspace');
return store.getters['workspace/workspacesById'][workspaceId];
},
getChanges() {
@ -252,7 +254,7 @@ export default new Provider({
return entries.map((entry) => {
const email = entry.author_email || entry.committer_email;
const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`;
userSvc.addInfo({
userSvc.addUserInfo({
id: sub,
name: entry.author_name || entry.committer_name,
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 utils from '../utils';
import workspaceSvc from '../workspaceSvc';
import badgeSvc from '../badgeSvc';
let fileIdToOpen;
let syncStartPageToken;
@ -137,6 +138,7 @@ export default new Provider({
await initFolder(token, folder);
}
badgeSvc.addBadge('addGoogleDriveWorkspace');
return getWorkspace(folderId);
},
async performAction() {

View File

@ -1,6 +1,7 @@
import networkSvc from '../../networkSvc';
import userSvc from '../../userSvc';
import store from '../../../store';
import badgeSvc from '../../badgeSvc';
const getAppKey = (fullAccess) => {
if (fullAccess) {
@ -73,7 +74,7 @@ export default {
method: 'POST',
url: 'https://api.dropboxapi.com/2/users/get_current_account',
});
userSvc.addInfo({
userSvc.addUserInfo({
id: `${subPrefix}:${body.account_id}`,
name: body.name.display_name,
imageUrl: body.profile_photo_url || '',
@ -96,8 +97,10 @@ export default {
store.dispatch('data/addDropboxToken', token);
return token;
},
addAccount(fullAccess = false) {
return this.startOauth2(fullAccess);
async addAccount(fullAccess = false) {
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 store from '../../../store';
import userSvc from '../../userSvc';
import badgeSvc from '../../badgeSvc';
const clientId = GITHUB_CLIENT_ID;
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
@ -90,7 +91,7 @@ export default {
access_token: accessToken,
},
})).body;
userSvc.addInfo({
userSvc.addUserInfo({
id: `${subPrefix}:${user.id}`,
name: user.login,
imageUrl: user.avatar_url || '',
@ -107,7 +108,7 @@ export default {
accessToken,
name: user.login,
sub: `${user.id}`,
repoFullAccess: scopes.indexOf('repo') !== -1,
repoFullAccess: scopes.includes('repo'),
};
// Add token to github tokens
@ -115,7 +116,9 @@ export default {
return token;
},
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 store from '../../../store';
import userSvc from '../../userSvc';
import badgeSvc from '../../badgeSvc';
const request = ({ accessToken, serverUrl }, options) => networkSvc.request({
...options,
@ -65,7 +66,7 @@ export default {
url: 'user',
});
const uniqueSub = `${serverUrl}/${user.id}`;
userSvc.addInfo({
userSvc.addUserInfo({
id: `${subPrefix}:${uniqueSub}`,
name: user.username,
imageUrl: user.avatar_url || '',
@ -88,8 +89,10 @@ export default {
store.dispatch('data/addGitlabToken', token);
return token;
},
addAccount(serverUrl, applicationId, sub = null) {
return this.startOauth2(serverUrl, applicationId, sub);
async addAccount(serverUrl, applicationId, sub = null) {
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 store from '../../../store';
import userSvc from '../../userSvc';
import badgeSvc from '../../badgeSvc';
const clientId = GOOGLE_CLIENT_ID;
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
@ -39,8 +40,8 @@ if (utils.queryParams.providerId === 'googleDrive') {
* https://developers.google.com/people/api/rest/v1/people/get
*/
const getUser = async (sub, token) => {
const url = `https://people.googleapis.com/v1/people/${sub}?personFields=names,photos`;
const { body } = await networkSvc.request(token
const url = `https://people.googleapis.com/v1/people/${sub}?personFields=names,photos&key=${apiKey}`;
const { body } = await networkSvc.request(sub === 'me' && token
? {
method: 'GET',
url,
@ -50,7 +51,7 @@ const getUser = async (sub, token) => {
}
: {
method: 'GET',
url: `${url}&key=${apiKey}`,
url,
}, true);
return body;
};
@ -60,8 +61,8 @@ userSvc.setInfoResolver('google', subPrefix, async (sub) => {
try {
const googleToken = Object.values(store.getters['data/googleTokensBySub'])[0];
const body = await getUser(sub, googleToken);
const name = body.names[0] || {};
const photo = body.photos[0] || {};
const name = (body.names && body.names[0]) || {};
const photo = (body.photos && body.photos[0]) || {};
return {
id: `${subPrefix}:${sub}`,
name: name.displayName,
@ -150,15 +151,15 @@ export default {
expiresOn: Date.now() + (expiresIn * 1000),
idToken,
sub: body.sub,
name: (existingToken || {}).name || 'Unknown',
name: (existingToken || {}).name || 'Someone',
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,
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
isBlogger: scopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
isPhotos: scopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
isDrive: scopes.includes('https://www.googleapis.com/auth/drive') ||
scopes.includes('https://www.googleapis.com/auth/drive.file'),
isBlogger: scopes.includes('https://www.googleapis.com/auth/blogger'),
isPhotos: scopes.includes('https://www.googleapis.com/auth/photos'),
driveFullAccess: scopes.includes('https://www.googleapis.com/auth/drive'),
};
// Call the user info endpoint
@ -169,7 +170,7 @@ export default {
if (name.displayName) {
token.name = name.displayName;
}
userSvc.addInfo({
userSvc.addUserInfo({
id: `${subPrefix}:${userId}`,
name: name.displayName,
imageUrl: (photo.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
@ -191,13 +192,17 @@ export default {
if (token.isLogin) {
try {
token.isSponsor = (await networkSvc.request({
const res = await networkSvc.request({
method: 'GET',
url: 'userInfo',
params: {
idToken: token.idToken,
},
})).body.sponsorUntil > Date.now();
});
token.isSponsor = res.body.sponsorUntil > Date.now();
if (token.isSponsor) {
badgeSvc.addBadge('sponsor');
}
} catch (err) {
// Ignore
}
@ -245,14 +250,20 @@ export default {
signin() {
return this.startOauth2(driveAppDataScopes);
},
addDriveAccount(fullAccess = false, sub = null) {
return this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }), sub);
async addDriveAccount(fullAccess = false, sub = null) {
const token = await this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }), sub);
badgeSvc.addBadge('addGoogleDriveAccount');
return token;
},
addBloggerAccount() {
return this.startOauth2(bloggerScopes);
async addBloggerAccount() {
const token = await this.startOauth2(bloggerScopes);
badgeSvc.addBadge('addBloggerAccount');
return token;
},
addPhotosAccount() {
return this.startOauth2(photosScopes);
async addPhotosAccount() {
const token = await this.startOauth2(photosScopes);
badgeSvc.addBadge('addGooglePhotosAccount');
return token;
},
async getSponsorship(token) {
const refreshedToken = await this.refreshToken(token);
@ -296,10 +307,10 @@ export default {
options.url = `https://www.googleapis.com/drive/v3/files/${fileId}`;
if (parents && oldParents) {
params.addParents = parents
.filter(parent => oldParents.indexOf(parent) === -1)
.filter(parent => !oldParents.includes(parent))
.join(',');
params.removeParents = oldParents
.filter(parent => parents.indexOf(parent) === -1)
.filter(parent => !parents.includes(parent))
.join(',');
}
} else if (parents) {
@ -457,7 +468,7 @@ export default {
},
});
revisions.forEach((revision) => {
userSvc.addInfo({
userSvc.addUserInfo({
id: `${subPrefix}:${revision.lastModifyingUser.permissionId}`,
name: revision.lastModifyingUser.displayName,
imageUrl: revision.lastModifyingUser.photoLink || '',

View File

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

View File

@ -1,5 +1,6 @@
import networkSvc from '../../networkSvc';
import store from '../../../store';
import badgeSvc from '../../badgeSvc';
const request = (token, options) => networkSvc.request({
...options,
@ -49,8 +50,10 @@ export default {
store.dispatch('data/addZendeskToken', token);
return token;
},
addAccount(subdomain, clientId) {
return this.startOauth2(subdomain, clientId);
async addAccount(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 providerRegistry from './providers/common/providerRegistry';
import workspaceSvc from './workspaceSvc';
import badgeSvc from './badgeSvc';
const hasCurrentFilePublishLocations = () => !!store.getters['publishLocation/current'].length;
@ -112,10 +113,11 @@ const requestPublish = () => {
if (networkSvc.isUserActive()) {
clearInterval(intervalId);
if (!hasCurrentFilePublishLocations()) {
// Cancel sync
// Cancel publish
throw new Error('Publish not possible.');
}
await publishFile(store.getters['file/current'].id);
badgeSvc.addBadge('triggerPublish');
}
};
intervalId = utils.setInterval(() => attempt(), 1000);
@ -123,7 +125,7 @@ const requestPublish = () => {
});
};
const createPublishLocation = (publishLocation) => {
const createPublishLocation = (publishLocation, featureId) => {
const currentFile = store.getters['file/current'];
publishLocation.fileId = currentFile.id;
store.dispatch(
@ -132,6 +134,9 @@ const createPublishLocation = (publishLocation) => {
const publishLocationToStore = await publish(publishLocation);
workspaceSvc.addPublishLocation(publishLocationToStore);
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 workspaceSvc from './workspaceSvc';
import constants from '../data/constants';
import badgeSvc from './badgeSvc';
const minAutoSyncEvery = 60 * 1000; // 60 sec
const inactivityThreshold = 3 * 1000; // 3 sec
@ -455,7 +456,7 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
if (serverContent &&
(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
newSyncHistoryItem[LAST_MERGED] = newSyncHistoryItem[LAST_SEEN];
@ -723,10 +724,11 @@ const syncWorkspace = async (skipContents = false) => {
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') {
await syncDataItem('settings');
await syncDataItem('workspaces');
await syncDataItem('badges');
}
await syncDataItem('templates');
@ -786,6 +788,10 @@ const syncWorkspace = async (skipContents = false) => {
if (syncContext.restartSkipContents) {
await syncWorkspace(true);
}
if (workspace.id === 'main') {
badgeSvc.addBadge('syncMainWorkspace');
}
} catch (err) {
if (err && err.message === 'TOO_LATE') {
// Restart sync
@ -799,7 +805,7 @@ const syncWorkspace = async (skipContents = false) => {
/**
* Enqueue a sync task, if possible.
*/
const requestSync = () => {
const requestSync = (addTriggerSyncBadge = false) => {
// No sync in light mode
if (store.state.light) {
return;
@ -851,6 +857,10 @@ const requestSync = () => {
workspaceSvc.deleteFile(fileId);
}
});
if (addTriggerSyncBadge) {
badgeSvc.addBadge('triggerSync');
}
} finally {
clearInterval(intervalId);
}

View File

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

View File

@ -1,6 +1,7 @@
import store from '../store';
import utils from './utils';
import constants from '../data/constants';
import badgeSvc from './badgeSvc';
const forbiddenFolderNameMatcher = /^\.stackedit-data$|^\.stackedit-trash$|\.md$|\.sync$|\.publish$/;
@ -249,8 +250,13 @@ export default {
...location,
id: utils.uid(),
});
// Sanitize the workspace
this.ensureUniqueLocations();
if (Object.keys(store.getters['syncLocation/currentWithWorkspaceSyncLocation']).length > 1) {
badgeSvc.addBadge('syncMultipleLocations');
}
},
addPublishLocation(location) {
@ -258,8 +264,13 @@ export default {
...location,
id: utils.uid(),
});
// Sanitize the workspace
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 utils from '../services/utils';
import cledit from '../services/editor/cledit';
import badgeSvc from '../services/badgeSvc';
const diffMatchPatch = new DiffMatchPatch();
@ -104,6 +105,7 @@ module.actions = {
...currentContent,
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 jekyllSiteTemplate from '../data/templates/jekyllSiteTemplate.html';
import constants from '../data/constants';
import features from '../data/features';
const itemTemplate = (id, data = {}) => ({
id,
@ -203,7 +204,19 @@ export default {
gitlabTokensBySub: (state, { tokensByType }) => tokensByType.gitlab || {},
wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {},
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: {
setSettings: setter('settings'),
@ -275,5 +288,6 @@ export default {
addGitlabToken: tokenAdder('gitlab'),
addWordpressToken: tokenAdder('wordpress'),
addZendeskToken: tokenAdder('zendesk'),
patchBadges: patcher('badges'),
},
};

View File

@ -3,7 +3,8 @@ import googleHelper from '../services/providers/helpers/googleHelper';
import syncSvc from '../services/syncSvc';
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;
return ids[idx % ids.length];
};

View File

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

View File

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

View File

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

View File

@ -6,8 +6,20 @@ export default {
itemsById: {},
},
mutations: {
addItem: ({ itemsById }, item) => {
Vue.set(itemsById, item.id, item);
setItem: ({ itemsById }, 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;
top: 0;
height: 100%;
& > * {
border-left: 2px solid transparent;
}
}
.gutter__background {
@ -289,8 +285,8 @@ textarea {
color: rgba(0, 0, 0, 0.33);
position: absolute;
left: 0;
padding: 2px 3px 2px 0;
width: 20px;
padding: 3px 3px 3px 0;
width: 22px;
height: 21px;
line-height: 1;