diff options
47 files changed, 888 insertions, 284 deletions
@@ -1,5 +1,5 @@ <!DOCTYPE html> -<html> +<html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> 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", @@ -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 @@ <router-link :to="{ name: 'root'}">{{sitename}}</router-link> </div> <div class='item right'> + <user-finder></user-finder> <router-link :to="{ name: 'settings'}"><i class="icon-cog"></i></router-link> </div> </div> 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 @@ <img class="base03-border" referrerpolicy="no-referrer" :src="attachment.large_thumb_url || attachment.url"/> </a> - <video v-if="type === 'video' && !hidden" :src="attachment.url" controls></video> + <video v-if="type === 'video' && !hidden" :src="attachment.url" controls loop></video> <audio v-if="type === 'audio'" :src="attachment.url" controls></audio> 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 @@ </div> <div class="panel-body"> <div class="timeline"> - <status v-for="status in conversation" @goto="setHighlight" :key="status.id" :statusoid="status" :expandable='false' :focused="focused(status.id)" :inConversation='true' :highlight="highlight"></status> + <status v-for="status in conversation" @goto="setHighlight" :key="status.id" @preview="setPreview" :statusoid="status" :expandable='false' :focused="focused(status.id)" :inConversation='true' :highlight="highlight" :replies="getReplies(status.id)"></status> + </div> + </div> + <div class="status-preview base00-background base03-border" :style="{ left: preview.x + 'px', top: preview.y + 'px'}" v-if="preview.status"> + <img class="avatar" :src="preview.status.user.profile_image_url_original"> + <div class="text"> + <h4> + {{ preview.status.user.name }} + <small><a>{{ preview.status.user.screen_name}}</a></small> + </h4> + <div @click.prevent="linkClicked" class="status-content" v-html="preview.status.statusnet_html"></div> </div> </div> </div> @@ -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; + } + } </style> 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 @@ <button @click.prevent="markAsSeen" class="base06 base02-background read-button">Read!</button> </div> <div class="panel-body base03-border"> - <div v-for="notification in visibleNotifications" class="notification" :class='{"unseen": !notification.seen}'> + <div v-for="notification in visibleNotifications" :key="notification" class="notification" :class='{"unseen": !notification.seen}'> <a :href="notification.action.user.statusnet_profile_url"> <img class='avatar' :src="notification.action.user.profile_image_url_original"> </a> - <div class='text'> - <timeago :since="notification.action.created_at" :auto-update="240"></timeago> + <div class='text' style="width: 100%;"> <div v-if="notification.type === 'favorite'"> - <h1>{{ notification.action.user.name }}<br><i class="fa icon-star"></i> favorited your <router-link :to="{ name: 'conversation', params: { id: notification.status.id } }">status</router-link></h1> - <p>{{ notification.status.text }}</p> + <h1> + <span :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span> + <i class="fa icon-star"></i> + <small><router-link :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small> + </h1> + <div v-html="notification.status.statusnet_html"></div> </div> <div v-if="notification.type === 'repeat'"> - <h1>{{ notification.action.user.name }}<br><i class="fa icon-retweet"></i> repeated your <router-link :to="{ name: 'conversation', params: { id: notification.status.id } }">status</router-link></h1> - <p>{{ notification.status.text }}</p> + <h1> + <span :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span> + <i class="fa icon-retweet lit"></i> + <small><router-link :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small> + </h1> + <div v-html="notification.status.statusnet_html"></div> </div> <div v-if="notification.type === 'mention'"> - <h1>{{ notification.action.user.name }}<br><i class="fa icon-reply"></i> <router-link :to="{ name: 'conversation', params: { id: notification.status.id } }">mentioned</router-link> you</h1> - <p>{{ notification.status.text }}</p> + <h1> + <span :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span> + <i class="fa icon-reply lit"></i> + <small><router-link :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small> + </h1> + <status :compact="true" :statusoid="notification.status"></status> </div> </div> </div> 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 `<img src="${item.original.profile_image_url}"></img> <div class='name'>${item.string}</div>` - }, - - // 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 @@ <template> <div class="post-status-form"> <form @submit.prevent="postStatus(newStatus)"> - <div class="form-group" > - <textarea v-model="newStatus.status" placeholder="Just landed in L.A." rows="3" class="form-control" @keyup.meta.enter="postStatus(newStatus)" @keyup.ctrl.enter="postStatus(newStatus)" @drop="fileDrop" @dragover.prevent="fileDrag"></textarea> + <div class="form-group base03-border" > + <textarea @click="setCaret" @keyup="setCaret" v-model="newStatus.status" placeholder="Just landed in L.A." rows="1" class="form-control" @keydown.meta.enter="postStatus(newStatus)" @keyup.ctrl.enter="postStatus(newStatus)" @drop="fileDrop" @dragover.prevent="fileDrag" @input="resize"></textarea> + </div> + <div style="position:relative;" v-if="candidates"> + <div class="autocomplete-panel base05-background"> + <div v-for="candidate in candidates" @click="replace('@' + candidate.screen_name + ' ')" class="autocomplete base01"> + <img :src="candidate.img"></img> + <span> + @{{candidate.screen_name}} + <small class="base02">{{candidate.name}}</small> + </span> + </div> + </div> + </div> + <div class='form-bottom'> + <media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="enableSubmit" :drop-files="dropFiles"></media-upload> + <button :disabled="submitDisabled" type="submit" class="btn btn-default base05 base01-background">Submit</button> </div> <div class="attachments"> <div class="attachment" v-for="file in newStatus.files"> @@ -13,10 +28,6 @@ <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a> </div> </div> - <div class='form-bottom'> - <media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="enableSubmit" :drop-files="dropFiles"></media-upload> - <button :disabled="submitDisabled" type="submit" class="btn btn-default base05 base01-background">Submit</button> - </div> </form> </div> </template> @@ -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 @@ <input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal"> <label for="hideNsfw">Enable clickthrough NSFW attachment hiding</label> </li> + <li> + <input type="checkbox" id="autoLoad" v-model="autoLoadLocal"> + <label for="autoLoad">Enable automatic loading when scrolled to the bottom</label> + </li> + <li> + <input type="checkbox" id="hoverPreview" v-model="hoverPreviewLocal"> + <label for="hoverPreview">Enable reply-link preview on mouse hover</label> + </li> </ul> </div> </div> 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 @@ <template> - <div class="status-el base00-background base03-border" v-if="!status.deleted" v-bind:class="[{ 'base01-background': isFocused }, { 'status-conversation': inConversation }]" > + <div class="status-el base00-background" v-if="compact"> + <div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></div> + <div v-if="loggedIn"> + <div class='status-actions'> + <div> + <a href="#" v-on:click.prevent="toggleReplying"> + <i class="fa icon-reply" :class="{'icon-reply-active': replying}"></i> + </a> + </div> + <retweet-button :status=status></retweet-button> + <favorite-button :status=status></favorite-button> + </div> + </div> + <post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" v-on:posted="toggleReplying" v-if="replying"/> + </div> + <div class="status-el base00-background base03-border" v-else-if="!status.deleted" v-bind:class="[{ 'base01-background': isFocused }, { 'status-conversation': inConversation }]" > <template v-if="muted"> <div class="media status container muted"> <small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small> <small class="muteWords">{{muteWordHits.join(', ')}}</small> - <a href="#" class="unmute" @click.prevent="toggleMute"><i class="icon-eye-off"></i></a> + <a href="#" class="unmute" @click.prevent="toggleMute"><i class="fa icon-eye-off"></i></a> </div> </template> <template v-if="!muted"> @@ -13,13 +28,14 @@ <i class='fa icon-retweet retweeted'></i> </div> <div class="media-body"> - Retweeted by {{retweeter}} + Repeated by <a :href="statusoid.user.statusnet_profile_url" style="font-weight: bold;" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a> </div> </div> <div class="media status container"> <div class="media-left"> <a :href="status.user.statusnet_profile_url"> - <img @click.prevent="toggleUserExpanded" class='avatar' :src="status.user.profile_image_url_original"> + <img @click.prevent="toggleUserExpanded" :class="{retweeted: retweet}" class='avatar' :src="status.user.profile_image_url_original"> + <img v-if="retweet" class='avatar-retweeter' :src="statusoid.user.profile_image_url_original"></img> </a> </div> <div class="media-body"> @@ -27,38 +43,45 @@ <user-card-content :user="status.user"></user-card-content> </div> <div class="user-content"> - <h4 class="media-heading"> - {{status.user.name}} - <small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small> - <small v-if="status.in_reply_to_screen_name"> > - <router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }"> - {{status.in_reply_to_screen_name}} - </router-link> - </small> - <template v-if="isReply && !expandable"> - <small> - <a href="#" @click.prevent="gotoOriginal" ><i class="icon-reply"></i></a> - </small> - </template> - - - <small> - <router-link :to="{ name: 'conversation', params: { id: status.id } }"> - <timeago :since="status.created_at" :auto-update="60"></timeago> - </router-link> - </small> - <template v-if="expandable"> - - - <small> - <a href="#" @click.prevent="toggleExpanded" ><i class="icon-plus-squared"></i></a> - </small> - </template> - <small v-if="unmuted"> - <a href="#" @click.prevent="toggleMute" ><i class="icon-eye-off"></i></a> - </small> - <small v-if="!status.is_local" class="source_url"> - <a :href="status.external_url" target="_blank" ><i class="icon-binoculars"></i></a> - </small> - </h4> + <div class="media-heading"> + <div class="name-and-links"> + <h4 class="user-name">{{status.user.name}}</h4> + <div class="links"> + <h4> + <small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small> + <small v-if="status.in_reply_to_screen_name"> > + <router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }"> + {{status.in_reply_to_screen_name}} + </router-link> + </small> + <template v-if="isReply && !expandable"> + <small> + <a href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)"><i class="icon-reply" @mouseenter="replyEnter(status.in_reply_to_status_id, $event)" @mouseout="replyLeave()"></i></a> + </small> + </template> + - + <small> + <router-link :to="{ name: 'conversation', params: { id: status.id } }"> + <timeago :since="status.created_at" :auto-update="60"></timeago> + </router-link> + </small> + </h4> + </div> + <h4 class="replies" v-if="inConversation"> + <small v-if="replies.length">Replies:</small> + <small v-for="reply in replies"> + <a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}} </a> + </small> + </h4> + </div> + <div class="heading-icons"> + <a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="fa icon-eye-off"></i></a> + <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="fa icon-binoculars"></i></a> + <template v-if="expandable"> + <a href="#" @click.prevent="toggleExpanded" class="expand"><i class="fa icon-plus-squared"></i></a> + </template> + </div> + </div> <div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></div> @@ -94,24 +117,65 @@ <style lang="scss"> @import '../../_variables.scss'; + + status-text-container { + display: block; +} + .status-el { hyphens: auto; overflow-wrap: break-word; word-wrap: break-word; word-break: break-word; border-left-width: 0px; + line-height: 18px; + + .notify { + .avatar { + border-width: 3px; + border-style: solid; + } + } + + .media-body { + flex: 1; + padding-left: 0.5em; + } + .user-content { + min-height: 52px; padding-top: 1px; } + .media-heading { + display: flex; + min-height: 1.4em; + margin-bottom: 0.3em; + + small { + font-weight: lighter; + } + h4 { + margin-right: 0.4em; + } + .name-and-links { + flex: 1 0; + display: flex; + flex-wrap: wrap; + } + .replies { + flex-basis: 100%; + } + } + .source_url { - float: right; + } - .greentext { - color: green; + .expand { + margin-right: -0.3em; } a { @@ -128,6 +192,34 @@ margin-top: 0.2em; margin-bottom: 0.5em; } + + .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; + } + } + } + } + + .greentext { + color: green; } .status-conversation { @@ -135,7 +227,14 @@ } .status-actions { - padding-top: 5px; + padding-top: 0.15em; + width: 100%; + display: flex; + + div, favorite-button { + max-width: 6em; + flex: 1; + } } .icon-reply:hover { @@ -147,7 +246,23 @@ } .status .avatar { - width: 48px; + width: 48px; + height: 48px; + + &.retweeted { + width: 40px; + height: 40px; + margin-right: 8px; + margin-bottom: 8px; + } + } + + .status img.avatar-retweeter { + width: 24px; + height: 24px; + position: absolute; + margin-left: 24px; + margin-top: 24px; } .status.compact .avatar { @@ -155,14 +270,15 @@ } .status { - padding: 0.65em 0.7em 0.8em 0.8em; + padding: 0.4em 0.7em 0.45em 0.7em; border-bottom: 1px solid; border-bottom-color: inherit; border-left: 4px rgba(255, 48, 16, 0.65); border-left-style: inherit; } + .muted { - padding: 0.1em 0.7em 0.1em 0.8em; + padding: 0.1em 0.4em 0.1em 0.8em; button { margin-left: auto; } @@ -194,4 +310,35 @@ flex: 1; } + @media all and (max-width: 960px) { + .status-el { + .name-and-links { + margin-left: -0.25em; + } + } + .status { + max-width: 100%; + } + + .status .avatar { + width: 40px; + height: 40px; + + &.retweeted { + width: 34px; + height: 34px; + margin-right: 8px; + margin-bottom: 8px; + } + } + + .status img.avatar-retweeter { + width: 22px; + height: 22px; + position: absolute; + margin-left: 18px; + margin-top: 18px; + } + } + </style> diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index d5a9adcc..3dc07f9e 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -6,7 +6,8 @@ const Timeline = { props: [ 'timeline', 'timelineName', - 'title' + 'title', + 'userId' ], computed: { timelineError () { return this.$store.state.statuses.error } @@ -20,11 +21,14 @@ const Timeline = { const credentials = store.state.users.currentUser.credentials const showImmediately = this.timeline.visibleStatuses.length === 0 + window.onscroll = this.scrollLoad + timelineFetcher.fetchAndUpdate({ store, credentials, timeline: this.timelineName, - showImmediately + showImmediately, + userId: this.userId }) }, methods: { @@ -40,8 +44,15 @@ const Timeline = { credentials, timeline: this.timelineName, older: true, - showImmediately: true + showImmediately: true, + userId: this.userId }).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false })) + }, + scrollLoad (e) { + let height = Math.max(document.body.offsetHeight, document.body.scrollHeight) + if (this.timeline.loading === false && this.$store.state.config.autoLoad && (window.innerHeight + window.pageYOffset) >= (height - 750)) { + this.fetchOlderStatuses() + } } } } diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue index bba16584..ff1b108c 100644 --- a/src/components/user_card_content/user_card_content.vue +++ b/src/components/user_card_content/user_card_content.vue @@ -61,10 +61,13 @@ props: [ 'user' ], computed: { headingStyle () { - let rgb = this.$store.state.config.colors['base00'].match(/\d+/g) - return { - backgroundColor: `rgb(${Math.floor(rgb[0] * 0.53)}, ${Math.floor(rgb[1] * 0.56)}, ${Math.floor(rgb[2] * 0.59)})`, - backgroundImage: `url(${this.user.cover_photo})` + let color = this.$store.state.config.colors['base00'] + if (color) { + let rgb = this.$store.state.config.colors['base00'].match(/\d+/g) + return { + backgroundColor: `rgb(${Math.floor(rgb[0] * 0.53)}, ${Math.floor(rgb[1] * 0.56)}, ${Math.floor(rgb[2] * 0.59)})`, + backgroundImage: `url(${this.user.cover_photo})` + } } }, bodyStyle () { diff --git a/src/components/user_finder/user_finder.js b/src/components/user_finder/user_finder.js new file mode 100644 index 00000000..03205382 --- /dev/null +++ b/src/components/user_finder/user_finder.js @@ -0,0 +1,22 @@ +const UserFinder = { + data: () => ({ + username: undefined, + hidden: true + }), + methods: { + findUser (username) { + this.$store.state.api.backendInteractor.externalProfile(username) + .then((user) => { + if (!user.error) { + this.$store.commit('addNewUsers', [user]) + this.$router.push({name: 'user-profile', params: {id: user.id}}) + } + }) + }, + toggleHidden () { + this.hidden = !this.hidden + } + } +} + +export default UserFinder diff --git a/src/components/user_finder/user_finder.vue b/src/components/user_finder/user_finder.vue new file mode 100644 index 00000000..c23d8ee0 --- /dev/null +++ b/src/components/user_finder/user_finder.vue @@ -0,0 +1,23 @@ +<template> + <a href="#" v-if="hidden"><i class="icon-user-plus user-finder-icon" @click.prevent="toggleHidden"/></a> + <span v-else> + <input class="user-finder-input base03-border" @keyup.enter="findUser(username)" v-model="username" placeholder="Find user" id="user-finder-input" type="text"/> + <i class="icon-cancel user-finder-icon" @click="toggleHidden"/> + </span> +</template> + +<script src="./user_finder.js"></script> + +<style lang="scss"> + .user-finder-icon { + margin-right: 0.25em; + } + + .user-finder-input { + border-width: 1px; + border-style: solid; + border-color: inherit; + border-radius: 5px; + padding: 0.1em 0.2em 0.2em 0.2em; + } +</style> diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 4d52bc95..5f9d4d08 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -1,16 +1,30 @@ import UserCardContent from '../user_card_content/user_card_content.vue' -import { find } from 'lodash' +import Timeline from '../timeline/timeline.vue' const UserProfile = { + created () { + this.$store.commit('clearTimeline', { timeline: 'user' }) + this.$store.dispatch('startFetching', ['user', this.userId]) + }, + destroyed () { + this.$store.dispatch('stopFetching', 'user') + }, computed: { + timeline () { return this.$store.state.statuses.timelines.user }, + userId () { + return this.$route.params.id + }, user () { - const id = this.$route.params.id - const user = find(this.$store.state.users.users, {id}) - return user + if (this.timeline.statuses[0]) { + return this.timeline.statuses[0].user + } else { + return false + } } }, components: { - UserCardContent + UserCardContent, + Timeline } } diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index 11a61bfc..9241c469 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -1,6 +1,9 @@ <template> - <div class="user-profile panel panel-default base00-background"> - <user-card-content :user="user"></user-card-content> + <div> + <div v-if="user" class="user-profile panel panel-default base00-background"> + <user-card-content :user="user"></user-card-content> + </div> + <Timeline :title="'User Timeline'" v-bind:timeline="timeline" v-bind:timeline-name="'user'" :user-id="userId"/> </div> </template> diff --git a/src/main.js b/src/main.js index cb59746d..dd59dea7 100644 --- a/src/main.js +++ b/src/main.js @@ -25,7 +25,7 @@ Vue.use(VueRouter) Vue.use(VueTimeago, { locale: 'en-US', locales: { - 'en-US': require('vue-timeago/locales/en-US.json') + 'en-US': require('../static/timeago.json') } }) @@ -34,6 +34,8 @@ const persistedStateOptions = { 'config.hideAttachments', 'config.hideAttachmentsInConv', 'config.hideNsfw', + 'config.autoLoad', + 'config.hoverPreview', 'config.muteWords', 'statuses.notifications', 'users.users' diff --git a/src/modules/api.js b/src/modules/api.js index a32adfde..e61382eb 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -1,4 +1,5 @@ import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' +import {isArray} from 'lodash' const api = { state: { @@ -18,9 +19,17 @@ const api = { }, actions: { startFetching (store, timeline) { + let userId = false + + // This is for user timelines + if (isArray(timeline)) { + userId = timeline[1] + timeline = timeline[0] + } + // Don't start fetching if we already are. if (!store.state.fetchers[timeline]) { - const fetcher = store.state.backendInteractor.startFetching({timeline, store}) + const fetcher = store.state.backendInteractor.startFetching({timeline, store, userId}) store.commit('addFetcher', {timeline, fetcher}) } }, diff --git a/src/modules/config.js b/src/modules/config.js index f59dc6f0..f7d6e9c8 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -7,6 +7,8 @@ const defaultState = { hideAttachments: false, hideAttachmentsInConv: false, hideNsfw: true, + autoLoad: true, + hoverPreview: true, muteWords: [] } diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 051ec71b..084800fa 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -1,4 +1,4 @@ -import { remove, slice, sortBy, toInteger, each, find, flatten, maxBy, last, merge, max, isArray } from 'lodash' +import { includes, remove, slice, sortBy, toInteger, each, find, flatten, maxBy, last, merge, max, isArray } from 'lodash' import apiService from '../services/api/api.service.js' // import parse from '../services/status_parser/status_parser.js' @@ -32,6 +32,17 @@ export const defaultState = { minVisibleId: 0, loading: false }, + user: { + statuses: [], + statusesObject: {}, + faves: [], + visibleStatuses: [], + visibleStatusesObject: {}, + newStatusCount: 0, + maxId: 0, + minVisibleId: 0, + loading: false + }, publicAndExternal: { statuses: [], statusesObject: {}, @@ -57,11 +68,15 @@ export const defaultState = { } } +const isNsfw = (status) => { + const nsfwRegex = /#nsfw/i + return includes(status.tags, 'nsfw') || !!status.text.match(nsfwRegex) +} + export const prepareStatus = (status) => { // Parse nsfw tags if (status.nsfw === undefined) { - const nsfwRegex = /#nsfw/i - status.nsfw = !!status.text.match(nsfwRegex) + status.nsfw = isNsfw(status) } // Set deleted flag @@ -242,6 +257,14 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us const uri = deletion.uri updateMaxId(deletion) + // Remove possible notification + const status = find(allStatuses, {uri}) + if (!status) { + return + } + + remove(state.notifications, ({action: {id}}) => id === status.id) + remove(allStatuses, { uri }) if (timeline) { remove(timelineObject.statuses, { uri }) @@ -276,6 +299,21 @@ export const mutations = { oldTimeline.visibleStatusesObject = {} each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status }) }, + clearTimeline (state, { timeline }) { + const emptyTimeline = { + statuses: [], + statusesObject: {}, + faves: [], + visibleStatuses: [], + visibleStatusesObject: {}, + newStatusCount: 0, + maxId: 0, + minVisibleId: 0, + loading: false + } + + state.timelines[timeline] = emptyTimeline + }, setFavorited (state, { status, value }) { const newStatus = state.allStatusesObject[status.id] newStatus.favorited = value diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index b0e8dd87..e848d076 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -19,9 +19,11 @@ const UNFOLLOWING_URL = '/api/friendships/destroy.json' const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json' const REGISTRATION_URL = '/api/account/register.json' const AVATAR_UPDATE_URL = '/api/qvitter/update_avatar.json' +const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json' +const QVITTER_USER_TIMELINE_URL = '/api/qvitter/statuses/user_timeline.json' // const USER_URL = '/api/users/show.json' -import { each } from 'lodash' +import { each, map } from 'lodash' const oldfetch = window.fetch @@ -88,6 +90,13 @@ const authHeaders = (user) => { } } +const externalProfile = (profileUrl) => { + let url = `${EXTERNAL_PROFILE_URL}?profileurl=${profileUrl}` + return fetch(url, { + method: 'GET' + }).then((data) => data.json()) +} + const followUser = ({id, credentials}) => { let url = `${FOLLOWING_URL}?user_id=${id}` return fetch(url, { @@ -143,24 +152,34 @@ const setUserMute = ({id, credentials, muted = true}) => { }) } -const fetchTimeline = ({timeline, credentials, since = false, until = false}) => { +const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false}) => { const timelineUrls = { public: PUBLIC_TIMELINE_URL, friends: FRIENDS_TIMELINE_URL, mentions: MENTIONS_URL, - 'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL + 'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL, + user: QVITTER_USER_TIMELINE_URL } let url = timelineUrls[timeline] + let params = [] + if (since) { - url += `?since_id=${since}` + params.push(['since_id', since]) } if (until) { - url += `?max_id=${until}` + params.push(['max_id', until]) + } + + if (userId) { + params.push(['user_id', userId]) } + const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&') + url += `?${queryString}` + return fetch(url, { headers: authHeaders(credentials) }).then((data) => data.json()) } @@ -253,7 +272,8 @@ const apiService = { setUserMute, fetchMutes, register, - updateAvatar + updateAvatar, + externalProfile } export default apiService diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index ceb559b6..5dbbf4b3 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -26,8 +26,8 @@ const backendInteractorService = (credentials) => { return apiService.unfollowUser({credentials, id}) } - const startFetching = ({timeline, store}) => { - return timelineFetcherService.startFetching({timeline, store, credentials}) + const startFetching = ({timeline, store, userId = false}) => { + return timelineFetcherService.startFetching({timeline, store, credentials, userId}) } const setUserMute = ({id, muted = true}) => { @@ -38,6 +38,7 @@ const backendInteractorService = (credentials) => { const register = (params) => apiService.register(params) const updateAvatar = ({params}) => apiService.updateAvatar({credentials, params}) + const externalProfile = (profileUrl) => apiService.externalProfile(profileUrl) const backendInteractorServiceInstance = { fetchStatus, @@ -51,7 +52,8 @@ const backendInteractorService = (credentials) => { setUserMute, fetchMutes, register, - updateAvatar + updateAvatar, + externalProfile } return backendInteractorServiceInstance diff --git a/src/services/completion/completion.js b/src/services/completion/completion.js new file mode 100644 index 00000000..8788d837 --- /dev/null +++ b/src/services/completion/completion.js @@ -0,0 +1,70 @@ +import { reduce, find } from 'lodash' + +export const replaceWord = (str, toReplace, replacement) => { + return str.slice(0, toReplace.start) + replacement + str.slice(toReplace.end) +} + +export const wordAtPosition = (str, pos) => { + const words = splitIntoWords(str) + const wordsWithPosition = addPositionToWords(words) + + return find(wordsWithPosition, ({start, end}) => start <= pos && end > pos) +} + +export const addPositionToWords = (words) => { + return reduce(words, (result, word) => { + const data = { + word, + start: 0, + end: word.length + } + + if (result.length > 0) { + const previous = result.pop() + + data.start += previous.end + data.end += previous.end + + result.push(previous) + } + + result.push(data) + + return result + }, []) +} + +export const splitIntoWords = (str) => { + // Split at word boundaries + const regex = /\b/ + const triggers = /[@#]+$/ + + let split = str.split(regex) + + // Add trailing @ and # to the following word. + const words = reduce(split, (result, word) => { + if (result.length > 0) { + let previous = result.pop() + const matches = previous.match(triggers) + if (matches) { + previous = previous.replace(triggers, '') + word = matches[0] + word + } + result.push(previous) + } + result.push(word) + + return result + }, []) + + return words +} + +const completion = { + wordAtPosition, + addPositionToWords, + splitIntoWords, + replaceWord +} + +export default completion diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 24aef069..b28de9e7 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -14,7 +14,7 @@ const update = ({store, statuses, timeline, showImmediately}) => { }) } -const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false}) => { +const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false}) => { const args = { timeline, credentials } const rootState = store.rootState || store.state const timelineData = rootState.statuses.timelines[camelCase(timeline)] @@ -25,14 +25,16 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false args['since'] = timelineData.maxId } + args['userId'] = userId + return apiService.fetchTimeline(args) .then((statuses) => update({store, statuses, timeline, showImmediately}), () => store.dispatch('setError', { value: true })) } -const startFetching = ({ timeline = 'friends', credentials, store }) => { - fetchAndUpdate({timeline, credentials, store, showImmediately: true}) - const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store }) +const startFetching = ({timeline = 'friends', credentials, store, userId = false}) => { + fetchAndUpdate({timeline, credentials, store, showImmediately: true, userId}) + const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store, userId }) return setInterval(boundFetchAndUpdate, 10000) } const timelineFetcher = { diff --git a/static/font/config.json b/static/font/config.json index 58eb1943..7c58cada 100644 --- a/static/font/config.json +++ b/static/font/config.json @@ -77,6 +77,18 @@ "css": "cog", "code": 59399, "src": "fontawesome" + }, + { + "uid": "1bafeeb1808a5fe24484c7890096901a", + "css": "user-plus", + "code": 62004, + "src": "fontawesome" + }, + { + "uid": "559647a6f430b3aeadbecd67194451dd", + "css": "menu", + "code": 61641, + "src": "fontawesome" } ] }
\ No newline at end of file diff --git a/static/font/css/fontello-codes.css b/static/font/css/fontello-codes.css index 3658db77..3e658de9 100644 --- a/static/font/css/fontello-codes.css +++ b/static/font/css/fontello-codes.css @@ -9,5 +9,7 @@ .icon-cog:before { content: '\e807'; } /* '' */ .icon-spin3:before { content: '\e832'; } /* '' */ .icon-spin4:before { content: '\e834'; } /* '' */ +.icon-menu:before { content: '\f0c9'; } /* '' */ .icon-reply:before { content: '\f112'; } /* '' */ -.icon-binoculars:before { content: '\f1e5'; } /* '' */
\ No newline at end of file +.icon-binoculars:before { content: '\f1e5'; } /* '' */ +.icon-user-plus:before { content: '\f234'; } /* '' */
\ No newline at end of file diff --git a/static/font/css/fontello-embedded.css b/static/font/css/fontello-embedded.css index 360bf238..7dedc03f 100644 --- a/static/font/css/fontello-embedded.css +++ b/static/font/css/fontello-embedded.css @@ -1,15 +1,15 @@ @font-face { font-family: 'fontello'; - src: url('../font/fontello.eot?36468641'); - src: url('../font/fontello.eot?36468641#iefix') format('embedded-opentype'), - url('../font/fontello.svg?36468641#fontello') format('svg'); + src: url('../font/fontello.eot?46746090'); + src: url('../font/fontello.eot?46746090#iefix') format('embedded-opentype'), + url('../font/fontello.svg?46746090#fontello') format('svg'); font-weight: normal; font-style: normal; } @font-face { font-family: 'fontello'; - src: url('data:application/octet-stream;base64,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') format('woff'), - url('data:application/octet-stream;base64,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') format('truetype'); + src: url('data:application/octet-stream;base64,d09GRgABAAAAABWUAA8AAAAAIrQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABWAAAADsAAABUIIslek9TLzIAAAGUAAAAQwAAAFY+L1N8Y21hcAAAAdgAAACuAAACVi0xhMdjdnQgAAACiAAAABMAAAAgBtX+5mZwZ20AAAKcAAAFkAAAC3CKkZBZZ2FzcAAACCwAAAAIAAAACAAAABBnbHlmAAAINAAACiUAAA46qDChZWhlYWQAABJcAAAAMgAAADYORd52aGhlYQAAEpAAAAAgAAAAJAfKA+1obXR4AAASsAAAACsAAAA8N9r//GxvY2EAABLcAAAAIAAAACAW7BrNbWF4cAAAEvwAAAAgAAAAIAEuDAtuYW1lAAATHAAAAXcAAALNzJ0dH3Bvc3QAABSUAAAAgwAAAK7ll8oIcHJlcAAAFRgAAAB6AAAAhuVBK7x4nGNgZGBg4GIwYLBjYHJx8wlh4MtJLMljkGJgYYAAkDwymzEnMz2RgQPGA8qxgGkOIGaDiAIAJjsFSAB4nGNgZN7JOIGBlYGBqYppDwMDQw+EZnzAYMjIBBRlYGVmwAoC0lxTGBxeMHwyYQ76n8UQxRzEsBQozAiSAwAAWwwzAHic7ZHNDcIwDIVfaGj5KRHqAEzACWUsBuLEHKzQQSr52HSB8hz7AMyAra9SXpM28gdgC6AhVxKB8ESA1oNpqHmDQ80j7lyf2dwvndwkz2MZyrTkdWWC3+SrAs9dPlqTDb8UeYMWHXbY8z9H9Dgh8WWLf/X1+fJV0hkbakYcThHiVDOO2hRHLYvDaUMczp0mDRqgQYMuINlQ+/No0A/KYOjtymTQGZZsIL0ByW05kwAAeJxjYEADEhDIHPQ/GoQBEhIDvwB4nK1WaXfTRhQdeUmchCwlCy1qYcTEabBGJmzBgAlBsmMgXZytlaCLFDvpvvGJ3+Bf82Tac+g3flrvGy8kkLTncJqTo3fnzdXM22USWpLYC+uRlJsvxdTWJo3sPAnphk3LUXwoO3shZYrJ3wVREK2W2rcdh0REIlC1rrBEEPseWZpkfOhRRsu2pFdNyi096S5b40G9Vd9+GjrKsTuhpGYzdGg9siVVGFWiSKY9UtKmZaj6K0krvL/CzFfNUMKITiJpvBnG0EjeG2e0ymg1tuMoimyy3ChSJJrhQRR5lNUS5+SKCQzKB82Q8sqnEeXD/Iis2KOcVrBLttP8vi95p3c5P7Ffb1G25EAfyI7s4Ox0JV+EW1th3LST7ShUEXbXd0Js2exU/2aP8ppGA7crMr3QjGCpfIUQKz+hzP4hWS2cT/mSR6NaspETQetlTuxLPoHW44gpcc0YWdDd0QkR1P2SMwz2mD4e/PHeKZYLEwJ4HMt6RyWcCBMpYXM0SdowcmAlZYsqqfWumDjldVrEW8J+7drRl85o41B3YjxbDx1bOVHJ8WhSp5lMndpJzaMpDaKUdCZ4zK8DKD+iSV5tYzWJlUfTOGbGhEQiAi3cS1NBLDuxpCkEzaMZvbkbprl2LVqkyQP13KP39OZWuLnTU9oO9LNGf1anYjrYC9PpaeQv8Wna5SJF6frpGX5M4kHWAjKRLTbDlIMHb/0O0svXlhyF1wbY7u3zK6h91kTwpAH7G9AeT9UpCUyFmFWIVkBirWtZlsnVrBapyNR3Q5pWvqzTBIpyHBfHvoxx/V8zM5aYEr7fidOzIy49c+1LCNMcfJt1PZrXqcVyAXFmeU6nWZbv6zTH8gOd5lme1+kIS1unoyw/1GmB5Uc6HWN5QQuadN/BkIsw5AIOkDCEpQNDWF6CISwVDGG5CENYFmEIyyUYwvJjGMJyGYawvKxl1dRTSePamVgGbEJgYo4eucxF5WoquVRCu2hUakOeEm6VVBTPqn9loF488oY5sBZIl8iaXzHOlY9G5fjWFS1vGjtXwLHqbx+O9jnxUtaLhT8F/9XWVCW9Ys3Dk6vwG4aebCeqNql4dE2Xz1U9uv5fVFRYC/QbSIVYKMqybHBnIoSPOp2GaqCVQ8xszDy063XLmp/D/TcxQhZQ/fg3FBoL3INOWUlZ7eCs1dfbstw7g3I4EyxJMTfz+lb4IiOz0n6RWcqej3wecAWMSmXYagOtFbzZJzEPmd4kzwRxW1E2SNrYzgSJDRzzgHnznQQmYeqqDeRO4YYN+AVhbsF5J1yieqMsh+5F7PMopPxbp+JE9qhojMCz2Rthr+9Cym9xDCQ0+aV+DFQVoakYNRXQNFJuqAZfxtm6bULGDvQjKnbDsqziw8cW95WSbRmEfKSI1aOjn9Zeok6q3H5mFJfvnb4FwSA1MX9733RxkMq7WskyR20DU7calVPXmkPjVYfq5lH1vePsEzlrmm66Jx56X9Oq28HFXCyw9m0O0lImF9T1YYUNosvFpVDqZTRJ77gHGBYY0O9Qio3/q/rYfJ4rVYXRcSTfTtS30edgDPwP2H9H9QPQ92Pocg0uz/eaE59u9OFsma6iF+un6Dcwa625WboG3NB0A+IhR62OuMoNfKcGcXqkuRzpIeBj3RXiAcAmgMXgE921jOZTAKP5jDk+wOfMYdBkDoMt5jDYZs4awA5zGOwyh8Eecxh8wZx1gC+ZwyBkDoOIOQyeMCcAeMocBl8xh8HXzGHwDXPuA3zLHAYxcxgkzGGwr+nWMMwtXtBdoLZBVaADU09Y3MPiUFNlyP6OF4b9vUHM/sEgpv6o6faQ+hMvDPVng5j6i0FM/VXTnSH1N14Y6u8GMfUPg5j6TL8Yy2UGv4x8lwoHlF1sPufvifcP28VAuQABAAH//wAPeJyNV1tsVNcVPfs87ntm7szcuXfsGc/T8/AD28wTMDHjF7aDiY2ZGmMCogkhwY5xIqWFJoDShEaNqob8tT9VGolEVR9SgDZfVfORICEi9ScJX/1J80OSlkj9aUrD0H3vmIdEU9Uzc+acs/e53mevvdfeQ4CQ21/Rj+krpJskGrFMhylzQmGKASV0HVB81IpbFhfRvpwVACkzCLI75Cs7oOAOtVIS6u5go9ix6ceBWbPfPH8eh1nT/TbvrQOB8+cDz9ju5K23Ag8qBgZcBcLRpgvsTTZAFBIkPaRBdjbGq/h/VULRqimiSuq6ApIsrROZyet4gPKmAIbmUkYOE87pEm7R2Ye2Z8vZTCm3NRrSRFdfrpL30wTU6ne+I5aUTWfyhWql5pQTMAylWr1cspnUByiSs64Ih/YtbXrVSlg02hn9qZUKUTse3Zmyv/nQSUDKvmHUsucyNd8NO/WuGj1nBc4FLDjnhIM3tYR2M9Tlt2koFeKdxp3Jq5fsVMrGAZLFYjIBC/ZNPGH7b/bjEe1mkOCfi81V9EODJEm1oYUCGmfCBWfXO/H5pYbhXpc8gz4x6EysoZG7iO3/Q5flMOH0AWISjlh+8FDLVyv1cMEdcx5iwmZvBq4OGRHj3zcN24ChD/1JiJ7RU8ZpiKbgcyNwufW5oZsgnz0rhzSugHM5YEREseU4rSJactc+1UWpEYx3WH6fqsiSYGD8X4Y21Fy3Y4dMJqw+qA8ChpBcd8Jts7OZbzGbvvDrL1eO/e03PR991MILONp/v0Dm7cwnn2Te/nJ9HS627xL/lpsQ7y5f8yF6iqTJOBlr7MgAl9w0QAtkkNZUkLgs8VUF80IGKh92o5Q3MdTIkgBczI417HQumrYjxbAXa5ZUwNAahAEoB7OZAdgIIjesIml3dief8pXaMFTT7Vk9XbKTkIBIEOOQXtWUW58LiWI2wirGh3IJ73ZRtf2rMKqK/RyeUC4aKf2SgjutP7k7mkKj3Duw6rd12QDKOBgwZ8e0a4ZxTYtbcE16WvzVp13z+a5pMfuavCp8GqoJqrDWRRt9gf64/T67Ti8grp1khEyQfWRfo1mJUcL3SpiCC+MU6PxYTwGTUAI+RQQX6+hChBnWCEj4XiUSw/cqYez4fa4irqdmd4UHOrqtLll09uXqA1Cv1CXZhkpezkgRyy7VMB3LmIlWRKLoomzGA3/A5Zv6CJRLTh3F6CVbtsPozrDtWAiSH7IorecL9QTyENT6hrZA5sXvHIIVU995xLTNiSHdvDr8xXBcaPKE2jH/SknXF7/5WamUFBrz6906qJGlmV/wm7pdaP7lVM+JK5OjB7PVx1L6sbnsykPj20bPvg5PYjocmdRNUx+aML/H4Vhr+VhJLUia3Nt9cnewN/Tyz7WaKkmWBKJ165EXYxDtOBQOd286vPKwdvbYkcaO7sdq4Xa8/Zml6A0SIDEy09jJXQgYFWuqTIXEhIShRgijhB0iEoC0QNDVS+hZQLYEYsbMWGdH1LEjVjgUVCUSAL/mJlHJjgTbrFYNohuhmq1mI9lIOVKu0j/2btvWe+tXxe3bizR/5ciVK0fojbsbS73bWiPu3pUrG/lwgS0zHTl4hUw2xp7YPzvKCR/WKJBKMWZyBqwNPEYA4esuXa0T5OB1NJphNNCjjx7Yu2dmuq83kwqHZGH3IcIZPyC+OSRXBFa2HdtC3AoY+kjmMrIuWl3IFzDpcfTQrnsZ5BI05k89fycEkrjAFxK2GwcYECVn42Gylz50eOHkAl18bhHiivyUpoeLkgjM+2R5d0enKnPzlGKYMWdOMqWdNhdKUQsoR2UFNPGU4ndybV1ld7RTVVjwFGZRIO7MiYA8bXGutpU1ODTcbH6/2Tzpys1EJFaS/FJkHsR2nzIbNzX5SdXYLqRGQvgloxSIxwJgyJ5uR2dqk2zI1vx9qvqwEOPxDdVOE+kTMbh96/YK+5ItkAKZJ2ca/m5bwsSeHaswziiSaxrJNU8wcCiwNRkYx2RfdWvkkoSxY0xjyoklIoRPIO1mH1DkLnyP3tOnM/sbTk8RyNTk5sHifM+8FTI0UoCC4gZWxkVIttC95VLV8z4moSNLLgAj4KIUAMQHMhEPSzdrC/Uawpf3gwvJDsTJLpfcg1h/BwFuvPzs2vikEJw3w6Ja3rvv8bnXKttUavxTtzS+jYbU0YnlR6HsCRcfn5+erA4rVP96Q6o1JpYPHn3p2eNj3jPYQmNk9fgPFZVC6LG9ewY3j2zZqoZZiam2+ZmiS9t35ost3halEg/K3NMvKQpGN/r99u0D7Av0e5KMkk2NXsxNAlObASbukRwcZ67PyAyQdMoKkyQk+R03bcY7uvHo2OgI7A/cOK7lCy6DbbhRstty9MVmXLnRXsjXXKU8/GPfnubE4tMrT67MjaXTUs7faZaDTKNZyOVfP3SgJaIBjrWpm3bnpw+8cOIHp7/rKq+ickrkFMkfYgtdia2TESuRmhtb3HtpT0/MhCALSMvv7z/4ej7XumFySfFW0we6M9GOPffpRtL+kNt7MMz/f7Fl+gHyU5IMknqjUgTMFNcB2IEJgq0IP4xqGFH0sNuqQBOd4jI8kNl8FV9lyeV3t4/aaLHc2sYi7brotVwb6+z96/2W+c1XXivEggEb/sfqyJQ39UYwp/0oDngj+Kf8tivA+Z0+6gK7jv1kH+lp5POA3DVFMIewcwRkLiyu615PgrpHs074IUeIDizgsp+22w8EploZEF5TmClgma5jLbflSDnl2Oz6FGhcyEEkRmZltowsLtZPWym19ZmuQ5cej9LT8Npy4tODb/CQyTUDc4nlk1uWG0OJkHQOazQksEXEAXvGT3d5tXcFbV0gJrFIDj2/lfyy4ZR7qKykQdCuiM/AvGVTHATsbFPAJslgOgeZYmOyRqjAArKKD5IFWVVBlpUlDRTFmOboBx9BJuj/dn1X5/h9h2SkhFQQu9F6tbR5U1+x0J1JJmIdQSuIZQdNDNR9ItIHWHHk4AbQwXCtnA5mg3B3w/1g1c5FstWKR9ni7gxetf2W2foMMfwJ98Ebr9Fh2+8t8f13H2+NXNaUNxUNTrS/6RutJkpa73nQQxdcN1on4cctwzuDO6P4+a3xu9OnFU1TvLEdyxf4SWZgLesnc+RE47n+HNXkVNLPGC2FKVewlIGMUSBr8rofiObTiG+N6D7q0+maAbjSNd9hCSj+ylAoO0wUzpUmURS+pAJX+CyQ3bsenp6cGN1RK28e7Cl2Z+IxJxIyNVVwooAS8LghPwIJKomyGzzWvR8ZD0F2I0PK3i8R5FWw7Ho50q7jlRHhlJBGSh7nOlnskuDV/Wfo8++elM7C+x9gTdGUDwzsGLTLWJcMCX21ipPWE71d5/JbW9HxBW6EEvltaV3vbx5p9uv6zNDprl44dOadF+mp3z8/8+DZ9kNb73X1w4/ij4wntozVtmQ6qZbBP63W20X+A3FLQFAAAAB4nGNgZGBgAGKz1uNX4/ltvjJwM78AijBcjThbBKP///0fzVLBHATkcjAwgUQBg8IN+AAAeJxjYGRgYA76n8XAwFL2/+//vywVDEARFMAPAKMNBr54nGN+wcDALAjECxCYRR9Ig8QVgDgSxP7/l/nl//8QNgSzlDEwAABWDg0DAAAAAAAASgDOAR4BhAIKArwDDAPOBIoFDAVyBcgGcgcdAAEAAAAPAGsABQAAAAAAAgAeAC4AcwAAAIgLcAAAAAB4nHWQ3WrCMBiG38yfbQrb2GCny9FQxuoPDEEQBIeebCcyPB211rZSG0mj4G3sHnYxu4ldy17bOIayljTP9+TLl68BcI1vCOTPE0fOAmeMcj7BKXqWC/TPlovkF8slVPFmuUz/brmCBwSWq7jBByuI4jmjBT4tC1yJS8snuBB3lgv0j5aL5J7lEm7Fq+UyvWe5golILVdxL74GarXVURAaWRvUZbvZ6sjpViqqKHFj6a5NqHQq+3KuEuPHsXI8tdzz2A/Wsav34X6e+DqNVCJbTnOvRn7ia9f4s131dBO0jZnLuVZLObQZcqXVwveMExqz6jYaf8/DAAorbKER8apCGEjUaOuc22iihQ5pygzJzDwrQgIXMY2LNXeE2UrKuM8xZ5TQ+syIyQ48fpdHfkwKuD9mFX20ehhPSLszosxL9uWwu8OsESnJMt3Mzn57T7HhaW1aw127LnXWlcTwoIbkfezWFjQevZPdiqHtosH3n//7AeZuhFEAeJxty80SgiAYRmFeFUyy7EZY1Q0RfjbOICA/03D3jbXtLJ7dYQ37Jdn/JjRo0YFDoMcJAyTOGHHBFRNuTBjtDFlRgvV67lLWUR4o2kKufaT8Jso9VVJ+WcZgS1JpLzrS3Br/4ims7v710W3kCo8UbJXP1XlTrI5pKImiOj7GPqu9JzIAeJxj8N7BcCIoYiMjY1/kBsadHAwcDMkFGxlYnTYxMDJogRibuZgYOSAsPgYwi81pF9MBoDQnkM3utIvBAcJmZnDZqMLYERixwaEjYiNzistGNRBvF0cDAyOLQ0dySARISSQQbOZhYuTR2sH4v3UDS+9GJgYXAAx2I/QAAA==') format('woff'), + url('data:application/octet-stream;base64,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') format('truetype'); } /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ @@ -17,7 +17,7 @@ @media screen and (-webkit-min-device-pixel-ratio:0) { @font-face { font-family: 'fontello'; - src: url('../font/fontello.svg?36468641#fontello') format('svg'); + src: url('../font/fontello.svg?46746090#fontello') format('svg'); } } */ @@ -62,5 +62,7 @@ .icon-cog:before { content: '\e807'; } /* '' */ .icon-spin3:before { content: '\e832'; } /* '' */ .icon-spin4:before { content: '\e834'; } /* '' */ +.icon-menu:before { content: '\f0c9'; } /* '' */ .icon-reply:before { content: '\f112'; } /* '' */ -.icon-binoculars:before { content: '\f1e5'; } /* '' */
\ No newline at end of file +.icon-binoculars:before { content: '\f1e5'; } /* '' */ +.icon-user-plus:before { content: '\f234'; } /* '' */
\ No newline at end of file diff --git a/static/font/css/fontello-ie7-codes.css b/static/font/css/fontello-ie7-codes.css index 9bd3bc9e..dfab853a 100644 --- a/static/font/css/fontello-ie7-codes.css +++ b/static/font/css/fontello-ie7-codes.css @@ -9,5 +9,7 @@ .icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } -.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
\ No newline at end of file +.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
\ No newline at end of file diff --git a/static/font/css/fontello-ie7.css b/static/font/css/fontello-ie7.css index a5745239..3e93ecd2 100644 --- a/static/font/css/fontello-ie7.css +++ b/static/font/css/fontello-ie7.css @@ -20,5 +20,7 @@ .icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } .icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } -.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
\ No newline at end of file +.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); } +.icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
\ No newline at end of file diff --git a/static/font/css/fontello.css b/static/font/css/fontello.css index 7b1fbd0c..81250ae3 100644 --- a/static/font/css/fontello.css +++ b/static/font/css/fontello.css @@ -1,11 +1,11 @@ @font-face { font-family: 'fontello'; - src: url('../font/fontello.eot?90538621'); - src: url('../font/fontello.eot?90538621#iefix') format('embedded-opentype'), - url('../font/fontello.woff2?90538621') format('woff2'), - url('../font/fontello.woff?90538621') format('woff'), - url('../font/fontello.ttf?90538621') format('truetype'), - url('../font/fontello.svg?90538621#fontello') format('svg'); + src: url('../font/fontello.eot?79576261'); + src: url('../font/fontello.eot?79576261#iefix') format('embedded-opentype'), + url('../font/fontello.woff2?79576261') format('woff2'), + url('../font/fontello.woff?79576261') format('woff'), + url('../font/fontello.ttf?79576261') format('truetype'), + url('../font/fontello.svg?79576261#fontello') format('svg'); font-weight: normal; font-style: normal; } @@ -15,7 +15,7 @@ @media screen and (-webkit-min-device-pixel-ratio:0) { @font-face { font-family: 'fontello'; - src: url('../font/fontello.svg?90538621#fontello') format('svg'); + src: url('../font/fontello.svg?79576261#fontello') format('svg'); } } */ @@ -65,5 +65,7 @@ .icon-cog:before { content: '\e807'; } /* '' */ .icon-spin3:before { content: '\e832'; } /* '' */ .icon-spin4:before { content: '\e834'; } /* '' */ +.icon-menu:before { content: '\f0c9'; } /* '' */ .icon-reply:before { content: '\f112'; } /* '' */ -.icon-binoculars:before { content: '\f1e5'; } /* '' */
\ No newline at end of file +.icon-binoculars:before { content: '\f1e5'; } /* '' */ +.icon-user-plus:before { content: '\f234'; } /* '' */
\ No newline at end of file diff --git a/static/font/demo.html b/static/font/demo.html index 98b49a84..02fb5d79 100644 --- a/static/font/demo.html +++ b/static/font/demo.html @@ -229,11 +229,11 @@ body { } @font-face { font-family: 'fontello'; - src: url('./font/fontello.eot?15442171'); - src: url('./font/fontello.eot?15442171#iefix') format('embedded-opentype'), - url('./font/fontello.woff?15442171') format('woff'), - url('./font/fontello.ttf?15442171') format('truetype'), - url('./font/fontello.svg?15442171#fontello') format('svg'); + src: url('./font/fontello.eot?13861244'); + src: url('./font/fontello.eot?13861244#iefix') format('embedded-opentype'), + url('./font/fontello.woff?13861244') format('woff'), + url('./font/fontello.ttf?13861244') format('truetype'), + url('./font/fontello.svg?13861244#fontello') format('svg'); font-weight: normal; font-style: normal; } @@ -315,8 +315,12 @@ body { <div class="row"> <div title="Code: 0xe832" class="the-icons span3"><i class="demo-icon icon-spin3 animate-spin"></i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div> <div title="Code: 0xe834" class="the-icons span3"><i class="demo-icon icon-spin4 animate-spin"></i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div> + <div title="Code: 0xf0c9" class="the-icons span3"><i class="demo-icon icon-menu"></i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div> <div title="Code: 0xf112" class="the-icons span3"><i class="demo-icon icon-reply"></i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div> + </div> + <div class="row"> <div title="Code: 0xf1e5" class="the-icons span3"><i class="demo-icon icon-binoculars"></i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div> + <div title="Code: 0xf234" class="the-icons span3"><i class="demo-icon icon-user-plus"></i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div> </div> </div> <div class="container footer">Generated by <a href="http://fontello.com">fontello.com</a></div> diff --git a/static/font/font/fontello.eot b/static/font/font/fontello.eot Binary files differindex 4050fa3b..4573d823 100644 --- a/static/font/font/fontello.eot +++ b/static/font/font/fontello.eot diff --git a/static/font/font/fontello.svg b/static/font/font/fontello.svg index f1e8b9fc..98105a87 100644 --- a/static/font/font/fontello.svg +++ b/static/font/font/fontello.svg @@ -26,9 +26,13 @@ <glyph glyph-name="spin4" unicode="" d="M498 850c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" /> +<glyph glyph-name="menu" unicode="" d="M857 100v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-14-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" /> + <glyph glyph-name="reply" unicode="" d="M1000 225q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" horiz-adv-x="1000" /> <glyph glyph-name="binoculars" unicode="" d="M393 671v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" /> + +<glyph glyph-name="user-plus" unicode="" d="M393 350q-89 0-152 63t-62 151 62 152 152 63 151-63 63-152-63-151-151-63z m536-71h196q7 0 13-6t5-12v-107q0-8-5-13t-13-5h-196v-197q0-7-6-12t-12-6h-107q-8 0-13 6t-5 12v197h-197q-7 0-12 5t-6 13v107q0 7 6 12t12 6h197v196q0 7 5 13t13 5h107q7 0 12-5t6-13v-196z m-411-125q0-29 21-51t50-21h143v-133q-38-28-95-28h-488q-67 0-108 39t-41 106q0 30 2 58t8 61 15 60 24 55 34 45 48 30 62 11q11 0 22-10 44-34 86-51t92-17 92 17 86 51q11 10 22 10 73 0 121-54h-125q-29 0-50-21t-21-50v-107z" horiz-adv-x="1142.9" /> </font> </defs> </svg>
\ No newline at end of file diff --git a/static/font/font/fontello.ttf b/static/font/font/fontello.ttf Binary files differindex bec32f07..c3bfb92a 100644 --- a/static/font/font/fontello.ttf +++ b/static/font/font/fontello.ttf diff --git a/static/font/font/fontello.woff b/static/font/font/fontello.woff Binary files differindex 245e1d2f..dced1f8c 100644 --- a/static/font/font/fontello.woff +++ b/static/font/font/fontello.woff diff --git a/static/font/font/fontello.woff2 b/static/font/font/fontello.woff2 Binary files differindex 9ec54aa4..b91fcbd7 100644 --- a/static/font/font/fontello.woff2 +++ b/static/font/font/fontello.woff2 diff --git a/static/timeago.json b/static/timeago.json new file mode 100644 index 00000000..b6f669c2 --- /dev/null +++ b/static/timeago.json @@ -0,0 +1,10 @@ +[ + "now", + ["%ss", "%ss"], + ["%smin", "%smin"], + ["%sh", "%sh"], + ["%sd", "%sd"], + ["%sw", "%sw"], + ["%sm", "%sm"], + ["%sy", "%sy"] +] diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js index 891423ca..d25cc108 100644 --- a/test/unit/specs/modules/statuses.spec.js +++ b/test/unit/specs/modules/statuses.spec.js @@ -125,18 +125,19 @@ describe('The Statuses module', () => { it('removes statuses by tag on deletion', () => { const state = cloneDeep(defaultState) const status = makeMockStatus({id: 1}) + const otherStatus = makeMockStatus({id: 3}) status.uri = 'xxx' const deletion = makeMockStatus({id: 2, is_post_verb: false}) deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.' deletion.uri = 'xxx' - mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) + mutations.addNewStatuses(state, { statuses: [status, otherStatus], showImmediately: true, timeline: 'public' }) mutations.addNewStatuses(state, { statuses: [deletion], showImmediately: true, timeline: 'public' }) - expect(state.allStatuses).to.eql([]) - expect(state.timelines.public.statuses).to.eql([]) - expect(state.timelines.public.visibleStatuses).to.eql([]) - expect(state.timelines.public.maxId).to.eql(2) + expect(state.allStatuses).to.eql([otherStatus]) + expect(state.timelines.public.statuses).to.eql([otherStatus]) + expect(state.timelines.public.visibleStatuses).to.eql([otherStatus]) + expect(state.timelines.public.maxId).to.eql(3) }) it('does not update the maxId when the noIdUpdate flag is set', () => { @@ -319,6 +320,36 @@ describe('The Statuses module', () => { expect(state.notifications[0].type).to.eql('mention') }) + it('removes a notification when the notice gets removed', () => { + const user = { id: 1 } + const state = cloneDeep(defaultState) + const status = makeMockStatus({id: 1}) + const otherStatus = makeMockStatus({id: 3}) + const mentionedStatus = makeMockStatus({id: 2}) + mentionedStatus.attentions = [user] + mentionedStatus.uri = 'xxx' + otherStatus.attentions = [user] + + const deletion = makeMockStatus({id: 4, is_post_verb: false}) + deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.' + deletion.uri = 'xxx' + + mutations.addNewStatuses(state, { statuses: [status, otherStatus], user }) + + expect(state.notifications.length).to.eql(1) + + mutations.addNewStatuses(state, { statuses: [mentionedStatus], user }) + expect(state.allStatuses.length).to.eql(3) + expect(state.notifications.length).to.eql(2) + expect(state.notifications[1].status).to.eql(mentionedStatus) + expect(state.notifications[1].action).to.eql(mentionedStatus) + expect(state.notifications[1].type).to.eql('mention') + + mutations.addNewStatuses(state, { statuses: [deletion], user }) + expect(state.allStatuses.length).to.eql(2) + expect(state.notifications.length).to.eql(1) + }) + it('adds the message to mentions when you are mentioned', () => { const user = { id: 1 } const state = cloneDeep(defaultState) diff --git a/test/unit/specs/services/completion/completion.spec.js b/test/unit/specs/services/completion/completion.spec.js new file mode 100644 index 00000000..8a41c653 --- /dev/null +++ b/test/unit/specs/services/completion/completion.spec.js @@ -0,0 +1,70 @@ +import { replaceWord, addPositionToWords, wordAtPosition, splitIntoWords } from '../../../../../src/services/completion/completion.js' + +describe('addPositiontoWords', () => { + it('adds the position to a word list', () => { + const words = ['hey', 'this', 'is', 'fun'] + + const expected = [ + { + word: 'hey', + start: 0, + end: 3 + }, + { + word: 'this', + start: 3, + end: 7 + }, + { + word: 'is', + start: 7, + end: 9 + }, + { + word: 'fun', + start: 9, + end: 12 + } + ] + + const res = addPositionToWords(words) + + expect(res).to.eql(expected) + }) +}) + +describe('splitIntoWords', () => { + it('splits at whitespace boundaries', () => { + const str = 'This is a #nice @test for you, @idiot.' + const expected = ['This', ' ', 'is', ' ', 'a', ' ', '#nice', ' ', '@test', ' ', 'for', ' ', 'you', ', ', '@idiot', '.'] + const res = splitIntoWords(str) + + expect(res).to.eql(expected) + }) +}) + +describe('wordAtPosition', () => { + it('returns the word for a given string and postion, plus the start and end position of that word', () => { + const str = 'Hey this is fun' + + const { word, start, end } = wordAtPosition(str, 4) + + expect(word).to.eql('this') + expect(start).to.eql(4) + expect(end).to.eql(8) + }) +}) + +describe('replaceWord', () => { + it('replaces a word (with start and end) with another word in a given string', () => { + const str = 'hey @take, how are you' + const wordsWithPosition = addPositionToWords(splitIntoWords(str)) + const toReplace = wordsWithPosition[2] + + expect(toReplace.word).to.eql('@take') + + const expected = 'hey @takeshitakenji, how are you' + const res = replaceWord(str, toReplace, '@takeshitakenji') + expect(res).to.eql(expected) + }) +}) @@ -5500,10 +5500,6 @@ tough-cookie@~2.3.0: dependencies: punycode "^1.4.1" -tributejs@^2.1.0: - version "2.3.3" - resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-2.3.3.tgz#ec3b9ae3edd0f7e2bc5ca56d11ae43fdd7a8cd28" - trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" @@ -5717,9 +5713,9 @@ vue-loader@^11.1.0: vue-style-loader "^2.0.0" vue-template-es2015-compiler "^1.2.2" -vue-router@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-2.2.1.tgz#b027f9fac2cf13462725e843d6dc631b6aa077f6" +vue-router@^2.5.3: + version "2.5.3" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-2.5.3.tgz#073783f564b6aece6c8a59c63e298dc2aabfb51b" vue-style-loader@^2.0.0: version "2.0.0" @@ -5728,9 +5724,9 @@ vue-style-loader@^2.0.0: hash-sum "^1.0.2" loader-utils "^0.2.7" -vue-template-compiler@^2.1.10: - version "2.1.10" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.1.10.tgz#cb89643adc395e97435585522e43d0a9b1913257" +vue-template-compiler@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.3.4.tgz#5a88ac2c5e4d5d6218e6aa80e7e221fb7e67894c" dependencies: de-indent "^1.0.2" he "^1.1.0" @@ -5743,13 +5739,13 @@ vue-timeago@^3.1.2: version "3.2.0" resolved "https://registry.yarnpkg.com/vue-timeago/-/vue-timeago-3.2.0.tgz#73fd0635de6ea4ecfbbce035b2e44035d806fba1" -vue@^2.1.0: - version "2.1.10" - resolved "https://registry.yarnpkg.com/vue/-/vue-2.1.10.tgz#c9235ca48c7925137be5807832ac4e3ac180427b" +vue@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.3.4.tgz#5ec3b87a191da8090bbef56b7cfabd4158038171" -vuex@^2.1.0: - version "2.1.2" - resolved "https://registry.yarnpkg.com/vuex/-/vuex-2.1.2.tgz#15d2da62dd6ff59c071f0a91cd4f434eacf6ca6c" +vuex@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/vuex/-/vuex-2.3.1.tgz#cde8e997c1f9957719bc7dea154f9aa691d981a6" watchpack@^0.2.1: version "0.2.9" |
