diff --git a/index.html b/index.html index 668b21bb..b7654cc1 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + diff --git a/package.json b/package.json index 74706389..d04c3e22 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "dev": "node build/dev-server.js", "build": "node build/build.js", "unit": "karma start test/unit/karma.conf.js --single-run", + "unit:watch": "karma start test/unit/karma.conf.js --single-run=false", "e2e": "node test/e2e/runner.js", "test": "npm run unit && npm run e2e", "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" @@ -22,12 +23,11 @@ "object-path": "^0.11.3", "sanitize-html": "^1.13.0", "sass-loader": "^4.0.2", - "tributejs": "^2.1.0", - "vue": "^2.1.0", - "vue-router": "^2.2.0", - "vue-template-compiler": "^2.1.10", + "vue": "^2.3.4", + "vue-router": "^2.5.3", + "vue-template-compiler": "^2.3.4", "vue-timeago": "^3.1.2", - "vuex": "^2.1.0" + "vuex": "^2.3.1" }, "devDependencies": { "autoprefixer": "^6.4.0", diff --git a/src/App.js b/src/App.js index 2a00b369..a2d891f7 100644 --- a/src/App.js +++ b/src/App.js @@ -1,13 +1,15 @@ import UserPanel from './components/user_panel/user_panel.vue' import NavPanel from './components/nav_panel/nav_panel.vue' import Notifications from './components/notifications/notifications.vue' +import UserFinder from './components/user_finder/user_finder.vue' export default { name: 'app', components: { UserPanel, NavPanel, - Notifications + Notifications, + UserFinder }, data: () => ({ mobileActivePanel: 'timeline' diff --git a/src/App.scss b/src/App.scss index 0a7e1ce5..a5f190cb 100644 --- a/src/App.scss +++ b/src/App.scss @@ -52,6 +52,8 @@ button{ .item { flex: 1; + line-height: 21px; + height: 21px; } .gaps > .item { @@ -134,11 +136,6 @@ main-router { background-color: rgba(0,0,0,0.1); } -.media-body { - flex: 1; - padding-left: 0.5em; -} - .container > * { min-width: 0px; } @@ -147,60 +144,6 @@ main-router { color: grey; } -.status-actions { - width: 50%; - display: flex; - - div, favorite-button { - flex: 1; - } -} - -status-text-container { - display: block; -} - -.status-el { - line-height: 18px; - - .notify { - .avatar { - border-width: 3px; - border-style: solid; - } - } - - .media-left { - img { - margin-top: 0.2em; - float: right; - margin-right: 0.3em; - border-radius: 5px; - } - } - - .retweet-info { - padding: 0.7em 0 0 0.6em; - - .media-left { - display: flex; - - i { - align-self: center; - text-align: right; - flex: 1; - padding-right: 0.3em; - } - } - } - - .media-heading { - small { - font-weight: lighter; - } - margin-bottom: 0.3em; - } -} nav { z-index: 1000; } diff --git a/src/App.vue b/src/App.vue index fcfdae97..b2d8df8b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -6,6 +6,7 @@ {{sitename}}
+
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index d50664b6..d45a6825 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -11,7 +11,7 @@ - + diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index e89fa84f..059028f9 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -1,4 +1,4 @@ -import { filter, sortBy } from 'lodash' +import { find, filter, sortBy } from 'lodash' import { statusType } from '../../modules/statuses.js' import Status from '../status/status.vue' @@ -10,7 +10,12 @@ const sortAndFilterConversation = (conversation) => { const conversation = { data () { return { - highlight: null + highlight: null, + preview: { + x: 0, + y: 0, + status: null + } } }, props: [ @@ -27,7 +32,6 @@ const conversation = { const conversationId = this.status.statusnet_conversation_id const statuses = this.$store.state.statuses.allStatuses const conversation = filter(statuses, { statusnet_conversation_id: conversationId }) - return sortAndFilterConversation(conversation) } }, @@ -46,6 +50,7 @@ const conversation = { const conversationId = this.status.statusnet_conversation_id this.$store.state.api.backendInteractor.fetchConversation({id: conversationId}) .then((statuses) => this.$store.dispatch('addNewStatuses', { statuses })) + .then(() => this.setHighlight(this.statusoid.id)) } else { const id = this.$route.params.id this.$store.state.api.backendInteractor.fetchStatus({id}) @@ -53,7 +58,21 @@ const conversation = { .then(() => this.fetchConversation()) } }, - focused: function (id) { + getReplies (id) { + let res = [] + id = Number(id) + let i + for (i = 0; i < this.conversation.length; i++) { + if (Number(this.conversation[i].in_reply_to_status_id) === id) { + res.push({ + name: `#${i}`, + id: this.conversation[i].id + }) + } + } + return res + }, + focused (id) { if (this.statusoid.retweeted_status) { return (id === this.statusoid.retweeted_status.id) } else { @@ -62,6 +81,15 @@ const conversation = { }, setHighlight (id) { this.highlight = Number(id) + }, + setPreview (id, x, y) { + if (id) { + this.preview.x = x + this.preview.y = y + this.preview.status = find(this.conversation, { id: id }) + } else { + this.preview.status = null + } } } } diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 8b1e00ac..e8d97f99 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -8,7 +8,17 @@
- + +
+
+
+ +
+

+ {{ preview.status.user.name }} + {{ preview.status.user.screen_name}} +

+
@@ -21,4 +31,30 @@ border-bottom-style: solid; border-bottom-width: 1px; } + + .status-preview { + position: absolute; + max-width: 35em; + padding: 0.5em; + display: flex; + border-color: inherit; + border-style: solid; + border-width: 1px; + border-radius: 4px; + box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5); + .avatar { + width: 32px; + height: 32px; + border-radius: 50%; + } + .text { + h4 { + margin-bottom: 0.4em; + small { + font-weight: lighter; + } + } + padding: 0 0.5em 0.5em 0.5em; + } + } diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index c8d5e212..c0c86c68 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -1,3 +1,5 @@ +import Status from '../status/status.vue' + import { sortBy, take, filter } from 'lodash' const Notifications = { @@ -23,6 +25,9 @@ const Notifications = { return this.unseenNotifications.length } }, + components: { + Status + }, watch: { unseenCount (count) { if (count > 0) { diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index c7c650a9..f02ced8d 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -45,19 +45,23 @@ word-wrap: break-word; line-height:18px; - .icon-retweet { + .icon-retweet.lit { color: $green; } - .icon-reply { + .icon-reply.lit { color: $blue; } h1 { + word-break: break-all; margin: 0 0 0.3em; padding: 0; font-size: 1em; line-height:20px; + small { + font-weight: lighter; + } } padding: 0.3em 0.8em 0.5em; diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue index a326e9cd..f5950ac9 100644 --- a/src/components/notifications/notifications.vue +++ b/src/components/notifications/notifications.vue @@ -7,23 +7,34 @@
-
+
-
- +
-

{{ notification.action.user.name }}
favorited your status

-

{{ notification.status.text }}

+

+ {{ notification.action.user.name }} + + +

+
-

{{ notification.action.user.name }}
repeated your status

-

{{ notification.status.text }}

+

+ {{ notification.action.user.name }} + + +

+
-

{{ notification.action.user.name }}
mentioned you

-

{{ notification.status.text }}

+

+ {{ notification.action.user.name }} + + +

+
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 01aeeb68..a8b4d39c 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -1,10 +1,8 @@ import statusPoster from '../../services/status_poster/status_poster.service.js' import MediaUpload from '../media_upload/media_upload.vue' import fileTypeService from '../../services/file_type/file_type.service.js' -import Tribute from '../../../node_modules/tributejs/src/Tribute.js' -require('../../../node_modules/tributejs/scss/tribute.scss') - -import { merge, reject, map, uniqBy } from 'lodash' +import Completion from '../../services/completion/completion.js' +import { take, filter, reject, map, uniqBy } from 'lodash' const buildMentionsString = ({user, attentions}, currentUser) => { let allAttentions = [...attentions] @@ -21,51 +19,6 @@ const buildMentionsString = ({user, attentions}, currentUser) => { return mentions.join(' ') + ' ' } -const defaultCollection = { - // symbol that starts the lookup - trigger: '@', - - // element to target for @mentions - iframe: null, - - // class added in the flyout menu for active item - selectClass: 'highlight', - - // function called on select that returns the content to insert - selectTemplate: function (item) { - return '@' + item.original.screen_name - }, - - // template for displaying item in menu - menuItemTemplate: function (item) { - return `
${item.string}
` - }, - - // template for when no match is found (optional), - // If no template is provided, menu is hidden. - noMatchTemplate: null, - - // specify an alternative parent container for the menu - menuContainer: document.body, - - // column to search against in the object (accepts function or string) - lookup: ({name, screen_name}) => `${name} (@${screen_name})`, // eslint-disable-line camelcase - - // column that contains the content to insert by default - fillAttr: 'screen_name', - - // REQUIRED: array of objects to match - values: [], - - // specify whether a space is required before the trigger character - requireLeadingSpace: true, - - // specify whether a space is allowed in the middle of mentions - allowSpaces: false -} - -const tribute = new Tribute({ collection: [] }) - const PostStatusForm = { props: [ 'replyTo', @@ -89,30 +42,48 @@ const PostStatusForm = { newStatus: { status: statusText, files: [] - } + }, + caret: 0 } }, computed: { + candidates () { + if (this.textAtCaret.charAt(0) === '@') { + const matchedUsers = filter(this.users, (user) => (user.name + user.screen_name).match(this.textAtCaret.slice(1))) + if (matchedUsers.length <= 0) { + return false + } + // eslint-disable-next-line camelcase + return map(take(matchedUsers, 5), ({screen_name, name, profile_image_url_original}) => ({ + screen_name: screen_name, + name: name, + img: profile_image_url_original + })) + } else { + return false + } + }, + textAtCaret () { + return (this.wordAtCaret || {}).word || '' + }, + wordAtCaret () { + const word = Completion.wordAtPosition(this.newStatus.status, this.caret - 1) || {} + return word + }, users () { return this.$store.state.users.users - }, - completions () { - let users = this.users - users = merge({values: users}, defaultCollection) - return [users] } }, - watch: { - completions () { - tribute.collection = this.completions - } - }, - mounted () { - const textarea = this.$el.querySelector('textarea') - tribute.collection = this.completions - tribute.attach(textarea) - }, methods: { + replace (replacement) { + this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement) + const el = this.$el.querySelector('textarea') + el.focus() + this.caret = 0 + }, + setCaret ({target: {selectionStart}}) { + this.caret = selectionStart + }, postStatus (newStatus) { statusPoster.postStatus({ status: newStatus.status, @@ -125,6 +96,8 @@ const PostStatusForm = { files: [] } this.$emit('posted') + let el = this.$el.querySelector('textarea') + el.style.height = '16px' }, addMediaFile (fileInfo) { this.newStatus.files.push(fileInfo) @@ -151,6 +124,13 @@ const PostStatusForm = { }, fileDrag (e) { e.dataTransfer.dropEffect = 'copy' + }, + resize (e) { + e.target.style.height = 'auto' + e.target.style.height = `${e.target.scrollHeight - 10}px` + if (e.target.value === '') { + e.target.style.height = '16px' + } } } } diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index e7143b62..a17d6479 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -1,8 +1,23 @@ @@ -44,14 +55,20 @@ .form-bottom { display: flex; padding: 0.5em; + height: 32px; button { - flex: 2; + width: 10em; } } .attachments { - padding: 0.5em; + padding: 0 0.5em; + + .attachment { + position: relative; + margin: 0.5em 0.8em 0.2em 0; + } i { position: absolute; @@ -91,11 +108,56 @@ form textarea { border: solid; border-width: 1px; - border-color: silver; + border-color: inherit; border-radius: 5px; line-height:16px; padding: 5px; - resize: vertical; + resize: none; + overflow: hidden; + } + + form textarea:focus { + min-height: 48px; + } + + .btn { + cursor: pointer; + } + + .btn[disabled] { + cursor: not-allowed; + } + + .icon-cancel { + cursor: pointer; + } + + .autocomplete-panel { + margin: 0 0.5em 0 0.5em; + border-radius: 5px; + position: absolute; + z-index: 1; + box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5); + min-width: 75%; + } + + .autocomplete { + cursor: pointer; + padding: 0.2em 0.4em 0.2em 0.4em; + border-bottom: 1px solid rgba(0, 0, 0, 0.4); + display: flex; + img { + width: 24px; + height: 24px; + border-radius: 2px; + } + span { + line-height: 24px; + margin: 0 0.1em 0 0.2em; + } + small { + font-style: italic; + } } } diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index e4ea56f6..b8aa876b 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -8,7 +8,10 @@ const settings = { hideAttachmentsInConvLocal: this.$store.state.config.hideAttachmentsInConv, hideNsfwLocal: this.$store.state.config.hideNsfw, muteWordsString: this.$store.state.config.muteWords.join('\n'), - previewfile: null + previewfile: null, + autoLoadLocal: this.$store.state.config.autoLoad, + hoverPreviewLocal: this.$store.state.config.hoverPreview, + muteWordsString: this.$store.state.config.muteWords.join('\n') } }, components: { @@ -58,6 +61,12 @@ const settings = { hideNsfwLocal (value) { this.$store.dispatch('setOption', { name: 'hideNsfw', value }) }, + autoLoadLocal (value) { + this.$store.dispatch('setOption', { name: 'autoLoad', value }) + }, + hoverPreviewLocal (value) { + this.$store.dispatch('setOption', { name: 'hoverPreview', value }) + }, muteWordsString (value) { value = filter(value.split('\n'), (word) => trim(word).length > 0) this.$store.dispatch('setOption', { name: 'muteWords', value }) diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index bdc0f351..f2442194 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -40,6 +40,14 @@ +
  • + + +
  • +
  • + + +
  • diff --git a/src/components/status/status.js b/src/components/status/status.js index 5e7bde53..4f5093e1 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -12,7 +12,9 @@ const Status = { 'expandable', 'inConversation', 'focused', - 'highlight' + 'highlight', + 'compact', + 'replies' ], data: () => ({ replying: false, @@ -86,9 +88,9 @@ const Status = { toggleReplying () { this.replying = !this.replying }, - gotoOriginal () { + gotoOriginal (id) { // only handled by conversation, not status_or_conversation - this.$emit('goto', this.status.in_reply_to_status_id) + this.$emit('goto', id) }, toggleExpanded () { this.$emit('toggleExpanded') @@ -98,6 +100,15 @@ const Status = { }, toggleUserExpanded () { this.userExpanded = !this.userExpanded + }, + replyEnter (id, event) { + if (this.$store.state.config.hoverPreview) { + let rect = event.target.getBoundingClientRect() + this.$emit('preview', Number(id), rect.left + 20, rect.top + 20 + window.pageYOffset) + } + }, + replyLeave () { + this.$emit('preview', 0, 0, 0) } }, watch: { @@ -107,9 +118,8 @@ const Status = { let rect = this.$el.getBoundingClientRect() if (rect.top < 100) { window.scrollBy(0, rect.top - 200) - } else if (rect.bottom > window.innerHeight - 100) { - // will be useful when scrolling down to replies or root posts is in - window.scrollBy(0, rect.bottom + 200) + } else if (rect.bottom > window.innerHeight - 50) { + window.scrollBy(0, rect.bottom - window.innerHeight + 50) } } } diff --git a/src/components/status/status.vue b/src/components/status/status.vue index db33a200..e582a80d 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -1,10 +1,25 @@