diff --git a/src/components/App.vue b/src/components/App.vue
index 52d41155..e9e1b7aa 100644
--- a/src/components/App.vue
+++ b/src/components/App.vue
@@ -14,6 +14,8 @@ import Layout from './Layout';
import Modal from './Modal';
import Notification from './Notification';
import SplashScreen from './SplashScreen';
+import timeSvc from '../services/timeSvc';
+import store from '../store';
// Global directives
Vue.directive('focus', {
@@ -48,6 +50,11 @@ Vue.directive('title', {
},
});
+// Global filters
+Vue.filter('formatTime', time =>
+ // Access the minute counter for reactive refresh
+ timeSvc.format(time, store.state.minuteCounter));
+
export default {
components: {
Layout,
diff --git a/src/components/Editor.vue b/src/components/Editor.vue
index 712631ae..7ad9dcd9 100644
--- a/src/components/Editor.vue
+++ b/src/components/Editor.vue
@@ -1,18 +1,22 @@
-
diff --git a/src/components/UserName.vue b/src/components/UserName.vue
new file mode 100644
index 00000000..e26a8a7f
--- /dev/null
+++ b/src/components/UserName.vue
@@ -0,0 +1,20 @@
+
+ {{name}}
+
+
+
diff --git a/src/components/common/PreviewClassApplier.js b/src/components/common/PreviewClassApplier.js
index 457ae42c..12019f1d 100644
--- a/src/components/common/PreviewClassApplier.js
+++ b/src/components/common/PreviewClassApplier.js
@@ -23,7 +23,9 @@ export default class PreviewClassApplier {
this.lastEltCount = this.eltCollection.length;
this.restoreClass = () => {
- if (!this.eltCollection.length || this.eltCollection.length !== this.lastEltCount) {
+ if (!editorSvc.sectionDescWithDiffsList) {
+ this.removeClass();
+ } else if (!this.eltCollection.length || this.eltCollection.length !== this.lastEltCount) {
this.removeClass();
this.applyClass();
}
diff --git a/src/components/common/app.scss b/src/components/common/app.scss
index b2d4af3a..ba899ad8 100644
--- a/src/components/common/app.scss
+++ b/src/components/common/app.scss
@@ -71,9 +71,9 @@ textarea {
height: auto;
padding: 6px 12px;
margin-bottom: 0;
- font-size: 18px;
font-weight: 400;
line-height: 1.4;
+ text-transform: uppercase;
overflow: hidden;
text-align: center;
white-space: nowrap;
@@ -212,6 +212,12 @@ textarea {
height: 100%;
}
+.gutter__background {
+ position: absolute;
+ height: 100%;
+ right: 0;
+}
+
.new-discussion-button {
color: rgba(0, 0, 0, 0.33);
position: absolute;
@@ -230,13 +236,13 @@ textarea {
.discussion-editor-highlighting,
.discussion-preview-highlighting {
- background-color: mix(#fff, $selection-highlighting-color, 50%);
+ background-color: mix(#fff, $selection-highlighting-color, 70%);
padding: 0.25em 0;
}
.discussion-editor-highlighting--hover,
.discussion-preview-highlighting--hover {
- background-color: mix(#fff, $selection-highlighting-color, 25%);
+ background-color: mix(#fff, $selection-highlighting-color, 50%);
* {
background-color: transparent;
@@ -245,7 +251,7 @@ textarea {
.discussion-editor-highlighting--selected,
.discussion-preview-highlighting--selected {
- background-color: mix(#fff, $selection-highlighting-color, 10%);
+ background-color: mix(#fff, $selection-highlighting-color, 20%);
* {
background-color: transparent;
@@ -256,10 +262,6 @@ textarea {
cursor: pointer;
}
-.discussion-preview-highlighting--selected {
- cursor: auto;
-}
-
.hidden-rendering-container {
position: absolute;
width: 500px;
diff --git a/src/components/gutters/Comment.vue b/src/components/gutters/Comment.vue
new file mode 100644
index 00000000..594020ce
--- /dev/null
+++ b/src/components/gutters/Comment.vue
@@ -0,0 +1,79 @@
+
+
+
+
+
diff --git a/src/components/gutters/CommentList.vue b/src/components/gutters/CommentList.vue
new file mode 100644
index 00000000..25f2dd0d
--- /dev/null
+++ b/src/components/gutters/CommentList.vue
@@ -0,0 +1,360 @@
+
+
+
+
+
+
+
diff --git a/src/components/gutters/CurrentDiscussion.vue b/src/components/gutters/CurrentDiscussion.vue
new file mode 100644
index 00000000..f957e0dc
--- /dev/null
+++ b/src/components/gutters/CurrentDiscussion.vue
@@ -0,0 +1,165 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/gutters/EditorNewDiscussionButtonGutter.vue b/src/components/gutters/EditorNewDiscussionButton.vue
similarity index 72%
rename from src/components/gutters/EditorNewDiscussionButtonGutter.vue
rename to src/components/gutters/EditorNewDiscussionButton.vue
index 6368196a..127d5e9b 100644
--- a/src/components/gutters/EditorNewDiscussionButtonGutter.vue
+++ b/src/components/gutters/EditorNewDiscussionButton.vue
@@ -1,24 +1,17 @@
-
+
+
+
diff --git a/src/components/gutters/PreviewNewDiscussionButton.vue b/src/components/gutters/PreviewNewDiscussionButton.vue
new file mode 100644
index 00000000..9a3eb248
--- /dev/null
+++ b/src/components/gutters/PreviewNewDiscussionButton.vue
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
diff --git a/src/components/gutters/PreviewNewDiscussionButtonGutter.vue b/src/components/gutters/PreviewNewDiscussionButtonGutter.vue
deleted file mode 100644
index fb1c1421..00000000
--- a/src/components/gutters/PreviewNewDiscussionButtonGutter.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-
-
-
-
-
-
diff --git a/src/components/gutters/StickyComment.vue b/src/components/gutters/StickyComment.vue
new file mode 100644
index 00000000..4ce94681
--- /dev/null
+++ b/src/components/gutters/StickyComment.vue
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
diff --git a/src/components/menus/common/MenuEntry.vue b/src/components/menus/common/MenuEntry.vue
index 0448bdb3..a5d125e8 100644
--- a/src/components/menus/common/MenuEntry.vue
+++ b/src/components/menus/common/MenuEntry.vue
@@ -16,6 +16,8 @@
text-align: left;
padding: 10px;
height: auto;
+ font-size: 17px;
+ text-transform: none;
div div {
text-decoration: underline;
diff --git a/src/components/modals/SponsorModal.vue b/src/components/modals/SponsorModal.vue
index bf3b66f1..32974d99 100644
--- a/src/components/modals/SponsorModal.vue
+++ b/src/components/modals/SponsorModal.vue
@@ -76,12 +76,14 @@ export default {
font-size: 2.3em;
margin: 0.75rem 0;
line-height: 1.2;
+ text-transform: none;
span {
display: inline-block;
font-size: 0.75rem;
opacity: 0.5;
white-space: normal;
+ line-height: 1.5;
}
.paypal-option__offer {
diff --git a/src/components/modals/TemplatesModal.vue b/src/components/modals/TemplatesModal.vue
index 2c100631..3c6930c2 100644
--- a/src/components/modals/TemplatesModal.vue
+++ b/src/components/modals/TemplatesModal.vue
@@ -12,16 +12,16 @@
-
diff --git a/src/components/modals/common/ModalInner.vue b/src/components/modals/common/ModalInner.vue
index 9528b527..c4b1a9dd 100644
--- a/src/components/modals/common/ModalInner.vue
+++ b/src/components/modals/common/ModalInner.vue
@@ -24,17 +24,22 @@ export default {
'config',
]),
showSponsorButton() {
- return !this.$store.getters.isSponsor && this.$store.getters['modal/config'].type !== 'sponsor';
+ const type = this.$store.getters['modal/config'].type;
+ return !this.$store.getters.isSponsor && type !== 'sponsor' && type !== 'signInForSponsorship';
},
},
methods: {
sponsor() {
Promise.resolve()
.then(() => !this.$store.getters['data/loginToken'] &&
- this.$store.dispatch('modal/signInRequired') // If user has to sign in
+ this.$store.dispatch('modal/signInForSponsorship') // If user has to sign in
.then(() => googleHelper.signin())
.then(() => syncSvc.requestSync()))
- .then(() => this.$store.dispatch('modal/open', 'sponsor'))
+ .then(() => {
+ if (!this.$store.getters.isSponsor) {
+ this.$store.dispatch('modal/open', 'sponsor');
+ }
+ })
.catch(() => { }); // Cancel
},
},
@@ -65,7 +70,7 @@ export default {
color: darken($error-color, 10%);
background-color: transparentize($error-color, 0.85);
border-radius: $border-radius-base;
- padding: 1em;
+ padding: 1em 1.5em;
margin-bottom: 1.2em;
}
diff --git a/src/libs/cleditHighlighter.js b/src/libs/cleditHighlighter.js
index 476e1ccf..c0e05303 100644
--- a/src/libs/cleditHighlighter.js
+++ b/src/libs/cleditHighlighter.js
@@ -168,8 +168,10 @@ function Highlighter(editor) {
}
this.addTrailingNode()
self.$trigger('highlighted')
- editor.selectionMgr.restoreSelection()
- editor.selectionMgr.updateCursorCoordinates()
+ if (editor.selectionMgr.hasFocus()) {
+ editor.selectionMgr.restoreSelection()
+ editor.selectionMgr.updateCursorCoordinates()
+ }
}.cl_bind(this))
return sectionList
diff --git a/src/services/editorSvc.js b/src/services/editorSvc.js
index 1967f17a..896b1276 100644
--- a/src/services/editorSvc.js
+++ b/src/services/editorSvc.js
@@ -11,6 +11,7 @@ import sectionUtils from './sectionUtils';
import extensionSvc from './extensionSvc';
import editorSvcDiscussions from './editorSvcDiscussions';
import editorSvcUtils from './editorSvcUtils';
+import utils from './utils';
import store from '../store';
const debounce = cledit.Utils.debounce;
@@ -77,6 +78,8 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
* Initialize the cledit editor with markdown-it section parser and Prism highlighter
*/
initClEditor() {
+ this.sectionDescMeasuredList = null;
+ this.sectionDescWithDiffsList = null;
const options = {
sectionHighlighter: section => Prism.highlight(
section.text, this.prismGrammars[section.data]),
@@ -509,7 +512,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
if (content.properties !== lastProperties) {
lastProperties = content.properties;
const options = extensionSvc.getOptions(store.getters['content/currentProperties']);
- if (JSON.stringify(options) !== JSON.stringify(this.options)) {
+ if (utils.serializeObject(options) !== utils.serializeObject(this.options)) {
this.options = options;
this.initPrism();
this.initConverter();
@@ -532,7 +535,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
immediate: true,
});
- store.watch(() => store.getters['layout/styles'],
+ store.watch(() => utils.serializeObject(store.getters['layout/styles']),
() => this.measureSectionDimensions(false, true));
this.initHighlighters();
diff --git a/src/services/editorSvcDiscussions.js b/src/services/editorSvcDiscussions.js
index 7d045d58..0c6c1518 100644
--- a/src/services/editorSvcDiscussions.js
+++ b/src/services/editorSvcDiscussions.js
@@ -201,7 +201,7 @@ export default {
Object.keys(discussions).forEach((discussionId) => {
const classApplier = oldEditorClassAppliers[discussionId] || new EditorClassApplier(
() => {
- const classes = [`discussion-editor-highlighting-${discussionId}`, 'discussion-editor-highlighting'];
+ const classes = [`discussion-editor-highlighting--${discussionId}`, 'discussion-editor-highlighting'];
if (store.state.discussion.currentDiscussionId === discussionId) {
classes.push('discussion-editor-highlighting--selected');
}
@@ -210,7 +210,7 @@ export default {
() => {
const startMarker = discussionMarkers[`${discussionId}:start`];
const endMarker = discussionMarkers[`${discussionId}:end`];
- return {
+ return startMarker && endMarker && {
start: startMarker.offset,
end: endMarker.offset,
};
@@ -229,7 +229,7 @@ export default {
Object.keys(discussions).forEach((discussionId) => {
const classApplier = oldPreviewClassAppliers[discussionId] || new PreviewClassApplier(
() => {
- const classes = [`discussion-preview-highlighting-${discussionId}`, 'discussion-preview-highlighting'];
+ const classes = [`discussion-preview-highlighting--${discussionId}`, 'discussion-preview-highlighting'];
if (store.state.discussion.currentDiscussionId === discussionId) {
classes.push('discussion-preview-highlighting--selected');
}
@@ -238,7 +238,7 @@ export default {
() => {
const startMarker = discussionMarkers[`${discussionId}:start`];
const endMarker = discussionMarkers[`${discussionId}:end`];
- return {
+ return startMarker && endMarker && {
start: this.getPreviewOffset(startMarker.offset),
end: this.getPreviewOffset(endMarker.offset),
};
diff --git a/src/services/editorSvcUtils.js b/src/services/editorSvcUtils.js
index 07d5da8f..a22814b2 100644
--- a/src/services/editorSvcUtils.js
+++ b/src/services/editorSvcUtils.js
@@ -1,4 +1,5 @@
import DiffMatchPatch from 'diff-match-patch';
+import cledit from '../libs/cledit';
import animationSvc from './animationSvc';
import store from '../store';
@@ -105,6 +106,24 @@ export default {
return editorOffset;
},
+ /**
+ * Get the coordinates of an offset in the preview
+ */
+ getPreviewOffsetCoordinates(offset) {
+ const start = cledit.Utils.findContainer(this.previewElt, offset && offset - 1);
+ const end = cledit.Utils.findContainer(this.previewElt, offset || offset + 1);
+ const range = document.createRange();
+ range.setStart(start.container, start.offsetInContainer);
+ range.setEnd(end.container, end.offsetInContainer);
+ const rect = range.getBoundingClientRect();
+ const contentRect = this.previewElt.getBoundingClientRect();
+ return {
+ top: Math.round((rect.top - contentRect.top) + this.previewElt.scrollTop),
+ height: Math.round(rect.height),
+ left: Math.round((rect.right - contentRect.left) + this.previewElt.scrollLeft),
+ };
+ },
+
/**
* Scroll the preview (or the editor if preview is hidden) to the specified anchor
*/
diff --git a/src/services/localDbSvc.js b/src/services/localDbSvc.js
index 1f7b4159..06ceb168 100644
--- a/src/services/localDbSvc.js
+++ b/src/services/localDbSvc.js
@@ -380,6 +380,7 @@ localDbSvc.sync()
// Wait for the next watch tick
return null;
}
+
return Promise.resolve()
// Load contentState from DB
.then(() => localDbSvc.loadContentState(currentFile.id))
@@ -388,8 +389,15 @@ localDbSvc.sync()
// Load content from DB
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`))
.then(
- // Success, set last opened file
- () => store.dispatch('data/setLastOpenedId', currentFile.id),
+ () => {
+ // Set last opened file
+ store.dispatch('data/setLastOpenedId', currentFile.id);
+ // Cancel new discussion
+ store.commit('discussion/setCurrentDiscussionId');
+ // Open the gutter if file contains discussions
+ store.commit('discussion/setCurrentDiscussionId',
+ store.getters['discussion/nextDiscussionId']);
+ },
(err) => {
// Failure (content is not available), go back to previous file
const lastOpenedFile = store.getters['file/lastOpened'];
diff --git a/src/services/networkSvc.js b/src/services/networkSvc.js
index 91e1f3c1..b42de77a 100644
--- a/src/services/networkSvc.js
+++ b/src/services/networkSvc.js
@@ -88,6 +88,7 @@ export default {
.then(() => {
const data = utils.parseQueryParams(`${event.data}`.slice(1));
if (data.error || data.state !== state) {
+ console.error(data); // eslint-disable-line no-console
reject('Could not get required authorization.');
} else {
resolve({
diff --git a/src/services/optional/scrollSync.js b/src/services/optional/scrollSync.js
index fb711385..94d285f3 100644
--- a/src/services/optional/scrollSync.js
+++ b/src/services/optional/scrollSync.js
@@ -166,10 +166,10 @@ editorSvc.$on('previewText', () => {
});
store.watch(
- () => store.getters['layout/styles'],
- (styles) => {
- isScrollEditor = styles.showEditor;
- isScrollPreview = !styles.showEditor;
+ () => store.getters['layout/styles'].showEditor,
+ (showEditor) => {
+ isScrollEditor = showEditor;
+ isScrollPreview = !showEditor;
skipAnimation = true;
});
diff --git a/src/services/providers/helpers/googleHelper.js b/src/services/providers/helpers/googleHelper.js
index 545ad6dc..e2cdc3b7 100644
--- a/src/services/providers/helpers/googleHelper.js
+++ b/src/services/providers/helpers/googleHelper.js
@@ -116,6 +116,7 @@ export default {
.then((res) => {
store.commit('userInfo/addItem', {
id: res.body.id,
+ name: res.body.displayName,
imageUrl: res.body.image.url,
});
return res.body;
diff --git a/src/services/syncSvc.js b/src/services/syncSvc.js
index 443de5a0..197829f6 100644
--- a/src/services/syncSvc.js
+++ b/src/services/syncSvc.js
@@ -155,11 +155,19 @@ function syncFile(fileId, syncContext = new SyncContext()) {
};
const isWelcomeFile = () => {
+ if (store.getters['data/syncDataByItemId'][`${fileId}/content`]) {
+ // If file has already been synced, keep on syncing
+ return false;
+ }
const file = getFile();
const content = getContent();
+ if (!file || !content) {
+ return false;
+ }
const welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
- const hash = content ? utils.hash(content.text) : 0;
- return file.name === 'Welcome file' && welcomeFileHashes[hash];
+ const hash = utils.hash(content.text);
+ const hasDiscussions = Object.keys(content.discussions).length;
+ return file.name === 'Welcome file' && welcomeFileHashes[hash] && !hasDiscussions;
};
const syncOneContentLocation = () => {
@@ -180,8 +188,8 @@ function syncFile(fileId, syncContext = new SyncContext()) {
// Skip welcome file if not synchronized explicitly
(syncLocations.length > 1 || !isWelcomeFile())
) {
- const token = provider.getToken(syncLocation);
- result = provider && token && store.dispatch('queue/doWithLocation', {
+ const token = provider && provider.getToken(syncLocation);
+ result = token && store.dispatch('queue/doWithLocation', {
location: syncLocation,
promise: provider.downloadContent(token, syncLocation)
.then((serverContent = null) => {
diff --git a/src/services/timeSvc.js b/src/services/timeSvc.js
new file mode 100644
index 00000000..8b157ed2
--- /dev/null
+++ b/src/services/timeSvc.js
@@ -0,0 +1,173 @@
+// Credit: https://github.com/github/time-elements/
+const weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
+const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
+
+const pad = num => `0${num}`.slice(-2);
+
+function strftime(time, formatString) {
+ const day = time.getDay();
+ const date = time.getDate();
+ const month = time.getMonth();
+ const year = time.getFullYear();
+ const hour = time.getHours();
+ const minute = time.getMinutes();
+ const second = time.getSeconds();
+ return formatString.replace(/%([%aAbBcdeHIlmMpPSwyYZz])/g, (_arg) => {
+ let match;
+ const modifier = _arg[1];
+ switch (modifier) {
+ case '%':
+ default:
+ return '%';
+ case 'a':
+ return weekdays[day].slice(0, 3);
+ case 'A':
+ return weekdays[day];
+ case 'b':
+ return months[month].slice(0, 3);
+ case 'B':
+ return months[month];
+ case 'c':
+ return time.toString();
+ case 'd':
+ return pad(date);
+ case 'e':
+ return date;
+ case 'H':
+ return pad(hour);
+ case 'I':
+ return pad(strftime(time, '%l'));
+ case 'l':
+ return hour === 0 || hour === 12 ? 12 : (hour + 12) % 12;
+ case 'm':
+ return pad(month + 1);
+ case 'M':
+ return pad(minute);
+ case 'p':
+ return hour > 11 ? 'PM' : 'AM';
+ case 'P':
+ return hour > 11 ? 'pm' : 'am';
+ case 'S':
+ return pad(second);
+ case 'w':
+ return day;
+ case 'y':
+ return pad(year % 100);
+ case 'Y':
+ return year;
+ case 'Z':
+ match = time.toString().match(/\((\w+)\)$/);
+ return match ? match[1] : '';
+ case 'z':
+ match = time.toString().match(/\w([+-]\d\d\d\d) /);
+ return match ? match[1] : '';
+ }
+ });
+}
+
+let dayFirst = null;
+let yearSeparator = null;
+
+// Private: Determine if the day should be formatted before the month name in
+// the user's current locale. For example, `9 Jun` for en-GB and `Jun 9`
+// for en-US.
+//
+// Returns true if the day appears before the month.
+function isDayFirst() {
+ if (dayFirst !== null) {
+ return dayFirst;
+ }
+
+ if (!('Intl' in window)) {
+ return false;
+ }
+
+ const options = { day: 'numeric', month: 'short' };
+ const formatter = new window.Intl.DateTimeFormat(undefined, options);
+ const output = formatter.format(new Date(0));
+
+ dayFirst = !!output.match(/^\d/);
+ return dayFirst;
+}
+
+// Private: Determine if the year should be separated from the month and day
+// with a comma. For example, `9 Jun 2014` in en-GB and `Jun 9, 2014` in en-US.
+//
+// Returns true if the date needs a separator.
+function isYearSeparator() {
+ if (yearSeparator !== null) {
+ return yearSeparator;
+ }
+
+ if (!('Intl' in window)) {
+ return true;
+ }
+
+ const options = { day: 'numeric', month: 'short', year: 'numeric' };
+ const formatter = new window.Intl.DateTimeFormat(undefined, options);
+ const output = formatter.format(new Date(0));
+
+ yearSeparator = !!output.match(/\d,/);
+ return yearSeparator;
+}
+
+// Private: Determine if the date occurs in the same year as today's date.
+//
+// date - The Date to test.
+//
+// Returns true if it's this year.
+function isThisYear(date) {
+ const now = new Date();
+ return now.getUTCFullYear() === date.getUTCFullYear();
+}
+
+class RelativeTime {
+ constructor(date) {
+ this.date = date;
+ }
+
+ toString() {
+ const ago = this.timeElapsed();
+ return ago || `on ${this.formatDate()}`;
+ }
+
+ timeElapsed() {
+ const ms = new Date().getTime() - this.date.getTime();
+ const sec = Math.round(ms / 1000);
+ const min = Math.round(sec / 60);
+ const hr = Math.round(min / 60);
+ const day = Math.round(hr / 24);
+ if (ms < 0) {
+ return 'just now';
+ } else if (sec < 45) {
+ return 'just now';
+ } else if (sec < 90) {
+ return 'a minute ago';
+ } else if (min < 45) {
+ return `${min} minutes ago`;
+ } else if (min < 90) {
+ return 'an hour ago';
+ } else if (hr < 24) {
+ return `${hr} hours ago`;
+ } else if (hr < 36) {
+ return 'a day ago';
+ } else if (day < 30) {
+ return `${day} days ago`;
+ }
+ return null;
+ }
+
+ formatDate() {
+ let format = isDayFirst() ? '%e %b' : '%b %e';
+ if (!isThisYear(this.date)) {
+ format += isYearSeparator() ? ', %Y' : ' %Y';
+ }
+ return strftime(this.date, format);
+ }
+}
+
+export default {
+ format(time) {
+ return time && new RelativeTime(new Date(time)).toString();
+ },
+};
diff --git a/src/services/userSvc.js b/src/services/userSvc.js
new file mode 100644
index 00000000..a8478588
--- /dev/null
+++ b/src/services/userSvc.js
@@ -0,0 +1,28 @@
+import googleHelper from '../services/providers/helpers/googleHelper';
+import store from '../store';
+
+const promised = {};
+
+export default {
+ getInfo(userId) {
+ if (!promised[userId]) {
+ // Try to find a token with this sub
+ const token = store.getters['data/googleTokens'][userId];
+ if (token) {
+ store.commit('userInfo/addItem', {
+ id: userId,
+ name: token.name,
+ });
+ }
+
+ // Get user info from Google
+ if (!store.state.offline) {
+ promised[userId] = true;
+ googleHelper.getUser(userId)
+ .catch(() => {
+ promised[userId] = false;
+ });
+ }
+ }
+ },
+};
diff --git a/src/store/data.js b/src/store/data.js
index 556d3e72..597a78fc 100644
--- a/src/store/data.js
+++ b/src/store/data.js
@@ -64,14 +64,16 @@ module.actions.toggleSidePreview = localSettingsToggler('showSidePreview');
module.actions.toggleStatusBar = localSettingsToggler('showStatusBar');
module.actions.toggleScrollSync = localSettingsToggler('scrollSync');
module.actions.toggleFocusMode = localSettingsToggler('focusMode');
-const notEnoughSpace = (rootGetters) => {
- const constants = rootGetters['layout/constants'];
+const notEnoughSpace = (getters) => {
+ const constants = getters['layout/constants'];
+ const showGutter = getters['discussion/currentDiscussion'];
return document.body.clientWidth < constants.editorMinWidth +
constants.explorerWidth +
constants.sideBarWidth +
- constants.buttonBarWidth;
+ constants.buttonBarWidth +
+ (showGutter ? constants.gutterWidth : 0);
};
-module.actions.toggleSideBar = ({ getters, dispatch, rootGetters }, value) => {
+module.actions.toggleSideBar = ({ commit, getters, dispatch, rootGetters }, value) => {
// Reset side bar
dispatch('setSideBarPanel');
// Close explorer if not enough space
@@ -83,7 +85,7 @@ module.actions.toggleSideBar = ({ getters, dispatch, rootGetters }, value) => {
}
dispatch('patchLocalSettings', patch);
};
-module.actions.toggleExplorer = ({ getters, dispatch, rootGetters }, value) => {
+module.actions.toggleExplorer = ({ commit, getters, dispatch, rootGetters }, value) => {
// Close side bar if not enough space
const patch = {
showExplorer: value === undefined ? !getters.localSettings.showExplorer : value,
diff --git a/src/store/discussion.js b/src/store/discussion.js
index 96bf7e82..3abacbb2 100644
--- a/src/store/discussion.js
+++ b/src/store/discussion.js
@@ -1,51 +1,148 @@
import utils from '../services/utils';
+const idShifter = offset => (state, getters) => {
+ const ids = Object.keys(getters.currentFileDiscussions);
+ const idx = ids.indexOf(state.currentDiscussionId) + offset + ids.length;
+ return ids[idx % ids.length];
+};
+
export default {
namespaced: true,
state: {
currentDiscussionId: null,
newDiscussion: null,
- newDiscussionId: '',
+ newDiscussionId: null,
+ newCommentText: '',
+ newCommentSelection: { start: 0, end: 0 },
+ isCommenting: false,
+ stickyComment: null,
},
mutations: {
setCurrentDiscussionId: (state, value) => {
- state.currentDiscussionId = value;
+ if (state.currentDiscussionId !== value) {
+ state.currentDiscussionId = value;
+ state.isCommenting = false;
+ }
},
setNewDiscussion: (state, value) => {
state.newDiscussion = value;
state.newDiscussionId = utils.uid();
state.currentDiscussionId = state.newDiscussionId;
+ state.isCommenting = true;
},
patchNewDiscussion: (state, value) => {
Object.assign(state.newDiscussion, value);
},
+ setNewCommentText: (state, value) => {
+ state.newCommentText = value || '';
+ },
+ setNewCommentSelection: (state, value) => {
+ state.newCommentSelection = value;
+ },
+ setIsCommenting: (state, value) => {
+ state.isCommenting = value;
+ if (!value) {
+ state.newDiscussionId = null;
+ }
+ },
+ setStickyComment: (state, value) => {
+ state.stickyComment = value;
+ },
},
getters: {
newDiscussion: state =>
state.currentDiscussionId === state.newDiscussionId && state.newDiscussion,
+ currentFileDiscussionLastComments: (state, getters, rootState, rootGetters) => {
+ const discussions = rootGetters['content/current'].discussions;
+ const comments = rootGetters['content/current'].comments;
+ const discussionLastComments = {};
+ Object.keys(comments).forEach((commentId) => {
+ const comment = comments[commentId];
+ if (discussions[comment.discussionId]) {
+ const lastComment = discussionLastComments[comment.discussionId];
+ if (!lastComment || lastComment.created < comment.created) {
+ discussionLastComments[comment.discussionId] = comment;
+ }
+ }
+ });
+ return discussionLastComments;
+ },
currentFileDiscussions: (state, getters, rootState, rootGetters) => {
- const currentContent = rootGetters['content/current'];
- const currentDiscussions = {
- ...currentContent.discussions,
- };
+ const currentFileDiscussions = {};
const newDiscussion = getters.newDiscussion;
if (newDiscussion) {
- currentDiscussions[state.newDiscussionId] = newDiscussion;
+ currentFileDiscussions[state.newDiscussionId] = newDiscussion;
}
- return currentDiscussions;
+ const discussions = rootGetters['content/current'].discussions;
+ const discussionLastComments = getters.currentFileDiscussionLastComments;
+ Object.keys(discussionLastComments)
+ .sort((id1, id2) =>
+ discussionLastComments[id2].created - discussionLastComments[id1].created)
+ .forEach((discussionId) => {
+ currentFileDiscussions[discussionId] = discussions[discussionId];
+ });
+ return currentFileDiscussions;
},
currentDiscussion: (state, getters) =>
getters.currentFileDiscussions[state.currentDiscussionId],
+ previousDiscussionId: idShifter(-1),
+ nextDiscussionId: idShifter(1),
+ currentDiscussionComments: (state, getters, rootState, rootGetters) => {
+ const comments = {};
+ if (getters.currentDiscussion) {
+ const contentComments = rootGetters['content/current'].comments;
+ Object.keys(contentComments)
+ .filter(commentId =>
+ contentComments[commentId].discussionId === state.currentDiscussionId)
+ .sort((id1, id2) =>
+ contentComments[id1].created - contentComments[id2].created)
+ .forEach((commentId) => {
+ comments[commentId] = contentComments[commentId];
+ });
+ }
+ return comments;
+ },
+ currentDiscussionLastCommentId: (state, getters) =>
+ Object.keys(getters.currentDiscussionComments).pop(),
+ currentDiscussionLastComment: (state, getters) =>
+ getters.currentDiscussionComments[getters.currentDiscussionLastCommentId],
},
actions: {
createNewDiscussion({ commit, rootGetters }, selection) {
if (selection) {
let text = rootGetters['content/current'].text.slice(selection.start, selection.end).trim();
- if (text.length > 250) {
- text = `${text.slice(0, 249).trim()}…`;
+ const maxLength = 80;
+ if (text.length > maxLength) {
+ text = `${text.slice(0, maxLength - 1).trim()}…`;
}
commit('setNewDiscussion', { ...selection, text });
}
},
+ cleanCurrentFile(
+ { getters, rootGetters, commit, dispatch },
+ { filterComment, filterDiscussion } = {},
+ ) {
+ const discussions = rootGetters['content/current'].discussions;
+ const comments = rootGetters['content/current'].comments;
+ const patch = {
+ discussions: {},
+ comments: {},
+ };
+ Object.keys(comments).forEach((commentId) => {
+ const comment = comments[commentId];
+ const discussion = discussions[comment.discussionId];
+ if (discussion && comment !== filterComment && discussion !== filterDiscussion) {
+ patch.discussions[comment.discussionId] = discussion;
+ patch.comments[commentId] = comment;
+ }
+ });
+
+ const nextDiscussionId = getters.nextDiscussionId;
+ dispatch('content/patchCurrent', patch, { root: true });
+ if (!getters.currentDiscussion) {
+ // Keep the gutter open
+ commit('setCurrentDiscussionId', nextDiscussionId);
+ }
+ },
},
};
diff --git a/src/store/index.js b/src/store/index.js
index a10e8fca..127468c5 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -46,6 +46,7 @@ const store = new Vuex.Store({
ready: false,
offline: false,
lastOfflineCheck: 0,
+ minuteCounter: 0,
monetizeSponsor: false,
},
getters: {
@@ -69,6 +70,9 @@ const store = new Vuex.Store({
updateLastOfflineCheck: (state) => {
state.lastOfflineCheck = Date.now();
},
+ updateMinuteCounter: (state) => {
+ state.minuteCounter += 1;
+ },
setMonetizeSponsor: (state, value) => {
state.monetizeSponsor = value;
},
@@ -121,4 +125,8 @@ const store = new Vuex.Store({
plugins: debug ? [createLogger()] : [],
});
+setInterval(() => {
+ store.commit('updateMinuteCounter');
+}, 60 * 1000);
+
export default store;
diff --git a/src/store/layout.js b/src/store/layout.js
index 0032dfc7..a3107bde 100644
--- a/src/store/layout.js
+++ b/src/store/layout.js
@@ -1,7 +1,5 @@
const minPadding = 20;
-const previewButtonWidth = 55;
const editorTopPadding = 10;
-const gutterWidth = 250;
const navigationBarEditButtonsWidth = (36 * 14) + 8; // 14 buttons + 1 spacer
const navigationBarLeftButtonWidth = 38 + 4 + 15;
const navigationBarRightButtonWidth = 38 + 8;
@@ -15,6 +13,7 @@ const minTitleMaxWidth = 200;
const constants = {
editorMinWidth: 320,
explorerWidth: 250,
+ gutterWidth: 250,
sideBarWidth: 280,
navigationBarHeight: 44,
buttonBarWidth: 26,
@@ -48,9 +47,9 @@ function computeStyles(state, getters, localSettings = getters['data/localSettin
}
let doublePanelWidth = styles.innerWidth - constants.buttonBarWidth;
- const showGutter = getters['discussion/currentDiscussion'];
+ const showGutter = !!getters['discussion/currentDiscussion'];
if (showGutter) {
- doublePanelWidth -= gutterWidth;
+ doublePanelWidth -= constants.gutterWidth;
}
if (doublePanelWidth < constants.editorMinWidth) {
doublePanelWidth = constants.editorMinWidth;
@@ -87,17 +86,17 @@ function computeStyles(state, getters, localSettings = getters['data/localSettin
const panelWidth = Math.floor(doublePanelWidth / 2);
styles.previewWidth = styles.showSidePreview ?
panelWidth :
- doublePanelWidth + constants.buttonBarWidth;
- let previewRightPadding = Math.max(
+ doublePanelWidth;
+ const previewRightPadding = Math.max(
Math.floor((styles.previewWidth - styles.textWidth) / 2), minPadding);
+ if (!styles.showSidePreview) {
+ styles.previewWidth += constants.buttonBarWidth;
+ }
styles.previewGutterWidth = showGutter && !localSettings.showEditor
- ? gutterWidth
+ ? constants.gutterWidth
: 0;
const previewLeftPadding = previewRightPadding + styles.previewGutterWidth;
styles.previewGutterLeft = previewLeftPadding - minPadding;
- if (!styles.showEditor && previewRightPadding < previewButtonWidth) {
- previewRightPadding = previewButtonWidth;
- }
styles.previewPadding = `${editorTopPadding}px ${previewRightPadding}px ${bottomPadding}px ${previewLeftPadding}px`;
styles.editorWidth = styles.showSidePreview ?
panelWidth :
@@ -105,7 +104,7 @@ function computeStyles(state, getters, localSettings = getters['data/localSettin
const editorRightPadding = Math.max(
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
styles.editorGutterWidth = showGutter && localSettings.showEditor
- ? gutterWidth
+ ? constants.gutterWidth
: 0;
const editorLeftPadding = editorRightPadding + styles.editorGutterWidth;
styles.editorGutterLeft = editorLeftPadding - minPadding;
diff --git a/src/store/modal.js b/src/store/modal.js
index aa11df57..7db38c2c 100644
--- a/src/store/modal.js
+++ b/src/store/modal.js
@@ -56,6 +56,16 @@ export default {
resolveText: 'Yes, delete',
rejectText: 'No',
}),
+ discussionDeletion: ({ dispatch }) => dispatch('open', {
+ content: 'You are about to delete a discussion. Are you sure?
',
+ resolveText: 'Yes, delete',
+ rejectText: 'No',
+ }),
+ commentDeletion: ({ dispatch }) => dispatch('open', {
+ content: 'You are about to delete a comment. Are you sure?
',
+ resolveText: 'Yes, delete',
+ rejectText: 'No',
+ }),
trashDeletion: ({ dispatch }) => dispatch('open', {
content: 'Files in the trash are automatically deleted after 7 days of inactivity.
',
resolveText: 'Ok',
@@ -67,14 +77,15 @@ export default {
}),
providerRedirection: ({ dispatch }, { providerName, onResolve }) => dispatch('open', {
content: `You are about to navigate to the ${providerName} authorization page.
`,
- resolveText: 'Ok, go on!',
+ resolveText: 'Ok, go on',
rejectText: 'Cancel',
onResolve,
}),
- signInRequired: ({ dispatch }) => dispatch('open', {
- content: `We have to sign you in with Google in order to activate your sponsorship.
- Note: This will backup and sync all your files and settings.
`,
- resolveText: 'Ok, sign in!',
+ signInForSponsorship: ({ dispatch }) => dispatch('open', {
+ type: 'signInForSponsorship',
+ content: `You have to sign in with Google to enable your sponsorship.
+ Note: This will sync all your files and settings.
`,
+ resolveText: 'Ok, sign in',
rejectText: 'Cancel',
}),
sponsorOnly: ({ dispatch }) => dispatch('open', {