diff options
Diffstat (limited to 'src/components')
19 files changed, 525 insertions, 99 deletions
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index 7715add5..ccf26b79 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -26,7 +26,7 @@ const Attachment = { autoHeight () { if (this.type === 'image' && this.nsfw) { return { - 'min-height': '311px' + 'min-height': '109px' } } } diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 6af23391..d50664b6 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -33,10 +33,10 @@ .attachments { display: flex; flex-wrap: wrap; - margin-right: -0.8em; + margin-right: -0.7em; .attachment { flex: 1 0 30%; - margin: 0.5em 0.8em 0.6em 0.0em; + margin: 0.5em 0.7em 0.6em 0.0em; align-self: flex-start; &.html { @@ -116,8 +116,10 @@ border-style: solid; border-width: 1px; border-radius: 5px; + object-fit: contain; width: 100%; height: 100%; /* If this isn't here, chrome will stretch the images */ + max-height: 500px; } } } diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 281b0183..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' @@ -8,6 +8,16 @@ const sortAndFilterConversation = (conversation) => { } const conversation = { + data () { + return { + highlight: null, + preview: { + x: 0, + y: 0, + status: null + } + } + }, props: [ 'statusoid', 'collapsable' @@ -22,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) } }, @@ -41,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}) @@ -48,12 +58,38 @@ 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 { return (id === this.statusoid.id) } + }, + 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 726cfb65..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" :key="status.id" :statusoid="status" :expandable='false' :focused="focused(status.id)" :inConversation='true'></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 9bc2a5ec..f02ced8d 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -1,6 +1,8 @@ @import '../../_variables.scss'; .notifications { + // a bit of a hack to allow scrolling below notifications + padding-bottom: 15em; .panel-heading { // force the text to stay centered, while keeping @@ -43,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 661d842c..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</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</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 797fcdbb..881a9d1c 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -5,6 +5,7 @@ import Completion from '../../services/completion/completion.js' import { take, filter, reject, map, uniqBy } from 'lodash' + const buildMentionsString = ({user, attentions}, currentUser) => { let allAttentions = [...attentions] @@ -87,6 +88,8 @@ const PostStatusForm = { files: [] } this.$emit('posted') + let el = this.$el.querySelector('textarea') + el.style.height = '16px' }, addMediaFile (fileInfo) { this.newStatus.files.push(fileInfo) @@ -113,6 +116,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 12a9c88a..4f6d4565 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -1,17 +1,8 @@ <template> <div class="post-status-form"> <form @submit.prevent="postStatus(newStatus)"> - <div class="form-group" > - <textarea @click="setCaret" @keyup="setCaret" 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> - <div class="attachments"> - <div class="attachment" v-for="file in newStatus.files"> - <i class="fa icon-cancel" @click="removeMediaFile(file)"></i> - <img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img> - <video v-if="type(file) === 'video'" :src="file.image" controls></video> - <audio v-if="type(file) === 'audio'" :src="file.image" controls></audio> - <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a> - </div> + <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> <h1>Word</h1> @@ -24,6 +15,15 @@ <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"> + <i class="fa icon-cancel" @click="removeMediaFile(file)"></i> + <img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img> + <video v-if="type(file) === 'video'" :src="file.image" controls></video> + <audio v-if="type(file) === 'audio'" :src="file.image" controls></audio> + <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a> + </div> + </div> </form> </div> </template> @@ -51,14 +51,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; @@ -86,11 +92,16 @@ 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 { diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index 3d373283..998aa354 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -1,11 +1,15 @@ import StyleSwitcher from '../style_switcher/style_switcher.vue' +import { filter, trim } from 'lodash' const settings = { data () { return { hideAttachmentsLocal: this.$store.state.config.hideAttachments, hideAttachmentsInConvLocal: this.$store.state.config.hideAttachmentsInConv, - hideNsfwLocal: this.$store.state.config.hideNsfw + hideNsfwLocal: this.$store.state.config.hideNsfw, + autoLoadLocal: this.$store.state.config.autoLoad, + hoverPreviewLocal: this.$store.state.config.hoverPreview, + muteWordsString: this.$store.state.config.muteWords.join('\n') } }, components: { @@ -20,6 +24,16 @@ 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 478d761a..af0242c4 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -9,6 +9,11 @@ <style-switcher></style-switcher> </div> <div class="setting-item"> + <h2>Filtering</h2> + <p>All notices containing these words will be muted, one per line</p> + <textarea id="muteWords" v-model="muteWordsString"></textarea> + </div> + <div class="setting-item"> <h2>Attachments</h2> <ul class="setting-list"> <li> @@ -23,6 +28,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> @@ -32,9 +45,13 @@ <script src="./settings.js"> </script> -<style> +<style lang="scss"> .setting-item { margin: 1em 1em 1.4em; + textarea { + width: 100%; + height: 100px; + } } .setting-list { list-style-type: none; diff --git a/src/components/status/status.js b/src/components/status/status.js index 87fff879..4f5093e1 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -4,13 +4,17 @@ import RetweetButton from '../retweet_button/retweet_button.vue' import DeleteButton from '../delete_button/delete_button.vue' import PostStatusForm from '../post_status_form/post_status_form.vue' import UserCardContent from '../user_card_content/user_card_content.vue' +import { filter } from 'lodash' const Status = { props: [ 'statusoid', 'expandable', 'inConversation', - 'focused' + 'focused', + 'highlight', + 'compact', + 'replies' ], data: () => ({ replying: false, @@ -19,6 +23,9 @@ const Status = { userExpanded: false }), computed: { + muteWords () { + return this.$store.state.config.muteWords + }, hideAttachments () { return (this.$store.state.config.hideAttachments && !this.inConversation) || (this.$store.state.config.hideAttachmentsInConv && this.inConversation) @@ -35,12 +42,30 @@ const Status = { loggedIn () { return !!this.$store.state.users.currentUser }, - muted () { return !this.unmuted && this.status.user.muted }, + muteWordHits () { + const statusText = this.status.text.toLowerCase() + const hits = filter(this.muteWords, (muteWord) => { + return statusText.includes(muteWord.toLowerCase()) + }) + + return hits + }, + muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) }, isReply () { return !!this.status.in_reply_to_status_id }, borderColor () { return { borderBottomColor: this.$store.state.config.colors['base02'] } + }, + isFocused () { + // retweet or root of an expanded conversation + if (this.focused) { + return true + } else if (!this.inConversation) { + return false + } + // use conversation highlight only when in conversation + return this.status.id === this.highlight } }, components: { @@ -63,6 +88,10 @@ const Status = { toggleReplying () { this.replying = !this.replying }, + gotoOriginal (id) { + // only handled by conversation, not status_or_conversation + this.$emit('goto', id) + }, toggleExpanded () { this.$emit('toggleExpanded') }, @@ -71,6 +100,28 @@ 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: { + 'highlight': function (id) { + id = Number(id) + if (this.status.id === id) { + let rect = this.$el.getBoundingClientRect() + if (rect.top < 100) { + window.scrollBy(0, rect.top - 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 62a55505..e582a80d 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -1,9 +1,25 @@ <template> - <div class="status-el base00-background base03-border" v-if="!status.deleted" v-bind:class="[{ 'base01-background': focused }, { '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> - <a href="#" class="unmute" @click.prevent="toggleMute"><i class="icon-eye-off"></i></a> + <small class="muteWords">{{muteWordHits.join(', ')}}</small> + <a href="#" class="unmute" @click.prevent="toggleMute"><i class="fa icon-eye-off"></i></a> </div> </template> <template v-if="!muted"> @@ -12,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"> @@ -26,40 +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"> + <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.in_reply_to_status_id } }"> - <i class="icon-reply"></i> - </router-link> + <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> - </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> - <small v-if="status.user.muted"> - <a href="#" @click.prevent="toggleMute" ><i class="icon-eye-off"></i></a> - </small> - </template> - <small v-if="!status.is_local" class="source_url"> - <a :href="status.external_url" target="_blank" ><i class="icon-binoculars"></i></a> - </small> - </h4> + </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> @@ -95,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 { @@ -129,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 { @@ -136,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 { @@ -148,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 { @@ -156,14 +270,22 @@ } .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 button { - margin-left: auto; + + .muted { + padding: 0.1em 0.4em 0.1em 0.8em; + button { + margin-left: auto; + } + + .muteWords { + margin-left: 10px; + } } a.unmute { @@ -188,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 8c971d53..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 () { @@ -79,9 +82,8 @@ return this.$store.state.users.currentUser }, dailyAvg () { - return Math.round( - this.user.statuses_count / ((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000)) - ) + const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000)) + return Math.round(this.user.statuses_count / days) } }, methods: { @@ -117,7 +119,6 @@ } .profile-panel-body { - padding-top: 0em; top: -0em; padding-top: 4em; } 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> |
