diff options
Diffstat (limited to 'src/components/status')
| -rw-r--r-- | src/components/status/status.js | 44 | ||||
| -rw-r--r-- | src/components/status/status.vue | 799 |
2 files changed, 467 insertions, 376 deletions
diff --git a/src/components/status/status.js b/src/components/status/status.js index 12f3bb25..73f4a7aa 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -4,9 +4,11 @@ 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 StillImage from '../still-image/still-image.vue' import { filter, find } from 'lodash' const Status = { + name: 'Status', props: [ 'statusoid', 'expandable', @@ -14,7 +16,10 @@ const Status = { 'focused', 'highlight', 'compact', - 'replies' + 'replies', + 'noReplyLinks', + 'noHeading', + 'inlineExpanded' ], data: () => ({ replying: false, @@ -22,7 +27,8 @@ const Status = { unmuted: false, userExpanded: false, preview: null, - showPreview: false + showPreview: false, + showingTall: false }), computed: { muteWords () { @@ -54,11 +60,6 @@ const Status = { }, 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) { @@ -68,6 +69,29 @@ const Status = { } // use conversation highlight only when in conversation return this.status.id === this.highlight + }, + // This is a bit hacky, but we want to approximate post height before rendering + // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them) + // as well as approximate line count by counting characters and approximating ~80 + // per line. + // + // Using max-height + overflow: auto for status components resulted in false positives + // very often with japanese characters, and it was very annoying. + hideTallStatus () { + if (this.showingTall) { + return false + } + const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80 + return lengthScore > 20 + }, + attachmentSize () { + if ((this.$store.state.config.hideAttachments && !this.inConversation) || + (this.$store.state.config.hideAttachmentsInConv && this.inConversation)) { + return 'hide' + } else if (this.compact) { + return 'small' + } + return 'normal' } }, components: { @@ -76,7 +100,8 @@ const Status = { RetweetButton, DeleteButton, PostStatusForm, - UserCardContent + UserCardContent, + StillImage }, methods: { linkClicked ({target}) { @@ -105,6 +130,9 @@ const Status = { toggleUserExpanded () { this.userExpanded = !this.userExpanded }, + toggleShowTall () { + this.showingTall = !this.showingTall + }, replyEnter (id, event) { this.showPreview = true const targetId = Number(id) diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 28272b0b..f1163fd9 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -1,123 +1,99 @@ <template> - <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="base09 icon-reply" :class="{'icon-reply-active': replying}"></i> - </a> - </div> - <retweet-button :loggedIn="loggedIn" :status=status></retweet-button> - <favorite-button :loggedIn="loggedIn" :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 status-fadein" v-else-if="!status.deleted" v-bind:class="[{ 'base01-background': isFocused }, { 'status-conversation': inConversation }]" > - <template v-if="muted"> + <div class="status-el" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]"> + <template v-if="muted && !noReplyLinks"> <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="base09 icon-eye-off"></i></a> + <a href="#" class="unmute" @click.prevent="toggleMute"><i class="icon-eye-off"></i></a> </div> </template> - <template v-if="!muted"> - <div v-if="retweet" class="media container retweet-info"> - <div class="media-left"> + <template v-else> + <div v-if="retweet && !noHeading" class="media container retweet-info"> + <StillImage v-if="retweet" class='avatar' :src="statusoid.user.profile_image_url_original"/> + <div class="media-body faint"> + <a :href="statusoid.user.statusnet_profile_url" style="font-weight: bold;" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a> <i class='fa icon-retweet retweeted'></i> - </div> - <div class="media-body"> - Repeated by <a :href="statusoid.user.statusnet_profile_url" style="font-weight: bold;" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a> + {{$t('timeline.repeated')}} </div> </div> - <div class="media status container"> - <div class="media-left"> - <a :href="status.user.statusnet_profile_url"> - <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> + + <div class="media status"> + <div v-if="!noHeading" class="media-left"> + <a :href="status.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded"> + <StillImage class='avatar' :class="{'avatar-compact': compact}" :src="status.user.profile_image_url_original"/> </a> </div> - <div class="media-body"> - <div class="base03-border usercard" v-if="userExpanded"> + <div class="status-body"> + <div class="usercard media-body" v-if="userExpanded"> <user-card-content :user="status.user" :switcher="false"></user-card-content> </div> - <div class="user-content"> - <div class="media-heading"> + <div v-if="!noHeading" class="media-body container media-heading"> + <div class="media-heading-left"> <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"> > + <span class="links"> + <router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link> + <span v-if="status.in_reply_to_screen_name" class="faint reply-info"> + <i class="icon-right-open"></i> <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"> - <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="base09 icon-eye-off"></i></a> - <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="base09 icon-binoculars"></i></a> - <template v-if="expandable"> - <a href="#" @click.prevent="toggleExpanded" class="expand"><i class="base09 icon-plus-squared"></i></a> - </template> + </span> + <a v-if="isReply && !noReplyLinks" 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> + </span> </div> + <h4 class="replies" v-if="inConversation && !noReplyLinks"> + <small v-if="replies.length">Replies:</small> + <small class="reply-link" 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="status-preview base00-background base03-border" v-if="showPreview && preview"> - <img class="avatar" :src="preview.user.profile_image_url_original"> - <div class="text"> - <h4> - {{ preview.user.name }} - <small><a>{{ preview.user.screen_name}}</a></small> - </h4> - <div @click.prevent="linkClicked" class="status-content" v-html="preview.statusnet_html"></div> - </div> + <div class="media-heading-right"> + <router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }"> + <timeago :since="status.created_at" :auto-update="60"></timeago> + </router-link> + <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="icon-link-ext"></i></a> + <template v-if="expandable"> + <a href="#" @click.prevent="toggleExpanded"><i class="icon-plus-squared"></i></a> + </template> + <a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="icon-eye-off"></i></a> </div> - <div class="status-preview status-preview-loading base00-background base03-border" v-else-if="showPreview"> - <i class="base09 icon-spin4 animate-spin"></i> + </div> + + <div v-if="showPreview" class="status-preview-container"> + <status class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status> + <div class="status-preview status-preview-loading" v-else> + <i class="icon-spin4 animate-spin"></i> </div> + </div> - <div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></div> + <div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper"> + <a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowTall">Show more</a> + <div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html"></div> + <a v-if="showingTall" href="#" class="tall-status-unhider" @click.prevent="toggleShowTall">Show less</a> + </div> - <div v-if='status.attachments' class='attachments'> - <attachment v-if="!hideAttachments" :status-id="status.id" :nsfw="status.nsfw" :attachment="attachment" v-for="attachment in status.attachments" :key="attachment.id"> - </attachment> - </div> + <div v-if='status.attachments' class='attachments media-body'> + <attachment :size="attachmentSize" :status-id="status.id" :nsfw="status.nsfw" :attachment="attachment" v-for="attachment in status.attachments" :key="attachment.id"> + </attachment> </div> - <div class='status-actions'> + <div v-if="!noHeading && !noReplyLinks" class='status-actions media-body'> <div v-if="loggedIn"> <a href="#" v-on:click.prevent="toggleReplying"> - <i class="base09 icon-reply" :class="{'icon-reply-active': replying}"></i> + <i class="icon-reply" :class="{'icon-reply-active': replying}"></i> </a> </div> - <retweet-button :loggedIn="loggedIn" :status=status></retweet-button> - <favorite-button :loggedIn="loggedIn" :status=status></favorite-button> - <delete-button :status=status></delete-button> + <retweet-button :loggedIn='loggedIn' :status='status'></retweet-button> + <favorite-button :loggedIn='loggedIn' :status='status'></favorite-button> + <delete-button :status='status'></delete-button> </div> </div> </div> - <div class="status base00-background container" v-if="replying"> + <div class="container" v-if="replying"> <div class="reply-left"/> <post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" v-on:posted="toggleReplying"/> </div> @@ -126,300 +102,387 @@ </template> <script src="./status.js" ></script> - <style lang="scss"> - @import '../../_variables.scss'; +@import '../../_variables.scss'; + +.status-body { + flex: 1; + min-width: 0; +} + +.status-preview.status-el { + border-style: solid; + border-width: 1px; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); +} + +.status-preview-container { + position: relative; + max-width: 100%; +} + +.status-preview { + position: absolute; + max-width: 95%; + display: flex; + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + border-style: solid; + border-width: 1px; + border-radius: $fallback--tooltipRadius; + border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5); + margin-top: 0.25em; + margin-left: 0.5em; + z-index: 50; + .status { + flex: 1; + border: 0; + min-width: 15em; + } +} + +.status-preview-loading { + display: block; + min-width: 15em; + padding: 1em; + text-align: center; + border-width: 1px; + border-style: solid; + i { + font-size: 2em; + } +} + +.status-el { + hyphens: auto; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; + border-left-width: 0px; + line-height: 18px; + min-width: 0; + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + + border-left: 4px $fallback--cRed; + border-left: 4px var(--cRed, $fallback--cRed); + + &_focused { + background-color: $fallback--lightBg; + background-color: var(--lightBg, $fallback--lightBg); + } - status-text-container { - display: block; + .timeline & { + border-bottom-width: 1px; + border-bottom-style: solid; } - .status-preview { - position: absolute; - max-width: 34em; - padding: 0.5em; + .media-body { + flex: 1; + padding: 0; + margin: 0 0 0.25em 0.8em; + } + + .media-heading { + flex-wrap: nowrap; + } + + .media-heading-left { + padding: 0; + vertical-align: bottom; + flex-basis: 100%; + + small { + font-weight: lighter; + } + h4 { + white-space: nowrap; + font-size: 14px; + margin-right: 0.25em; + overflow: hidden; + text-overflow: ellipsis; + } + .name-and-links { + padding: 0; + flex: 1 0; + display: flex; + flex-wrap: wrap; + align-content: center; + } + .links { + display: flex; + padding-top: 1px; + margin-left: 0.2em; + font-size: 12px; + color: $fallback--link; + color: var(--link, $fallback--link); + max-width: 100%; + a { + max-width: 100%; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + } + .reply-info { + display: flex; + } + .replies { + line-height: 16px; + } + .reply-link { + margin-right: 0.2em; + } + } + + .media-heading-right { + flex-shrink: 0; 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); - margin-top: 0.5em; - margin-left: 1em; + flex-wrap: nowrap; + max-height: 1.5em; + margin-left: 0.25em; + .timeago { + margin-right: 0.2em; + font-size: 12px; + padding-top: 1px; + } + i { + margin-left: 0.2em; + } + } + a { + display: inline-block; + word-break: break-all; + } + + .tall-status { + position: relative; + height: 220px; + overflow-x: hidden; + overflow-y: hidden; + } + + .tall-status-hider { + position: absolute; + height: 70px; + margin-top: 150px; + width: 100%; + text-align: center; + line-height: 110px; + background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%); + background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%); + &_focused { + background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--lightBg 80%); + background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--lightBg, $fallback--lightBg) 80%); + } + } + + .tall-status-unhider { + width: 100%; + text-align: center; + } + + .status-content { + margin-right: 0.5em; + img, video { + max-width: 100%; + max-height: 400px; + vertical-align: middle; + object-fit: contain; + } + + blockquote { + margin: 0.2em 0 0.2em 2em; + font-style: italic; + } + + p { + margin: 0; + margin-top: 0.2em; + margin-bottom: 0.5em; + } + } + + .retweet-info { + padding: 0.4em 0.6em 0 0.6em; + margin: 0 0 -0.5em 0; .avatar { - flex-shrink: 0; - width: 32px; - height: 32px; - border-radius: 50%; + border-radius: $fallback--avatarAltRadius; + border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + margin-left: 28px; + width: 20px; + height: 20px; } - .text { - h4 { - margin-bottom: 0.4em; - small { - font-weight: lighter; - } + + .media-body { + font-size: 1em; + line-height: 22px; + + display: flex; + align-content: center; + flex-wrap: wrap; + i { + padding: 0 0.2em; + } + a { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } - padding: 0 0.5em 0.5em 0.5em; } } +} - .status-preview-loading { - display: block; - font-size: 2em; - min-width: 8em; - text-align: center; +.status-fadein { + animation-duration: 0.4s; + animation-name: fadein; +} + +@keyframes fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.greentext { + color: green; +} + +.status-conversation { + border-left-style: solid; +} + +.status-actions { + width: 100%; + display: flex; + + div, favorite-button { + padding-top: 0.25em; + max-width: 6em; + flex: 1; + } +} + +.icon-reply:hover { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); +} + +.icon-reply.icon-reply-active { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); +} + +.status .avatar-compact { + width: 32px; + height: 32px; + border-radius: $fallback--avatarAltRadius; + border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); +} + +.avatar { + width: 48px; + height: 48px; + border-radius: $fallback--avatarRadius; + border-radius: var(--avatarRadius, $fallback--avatarRadius); + overflow: hidden; + position: relative; + + img { + width: 100%; + height: 100%; + } + + &.animated::before { + display: none; + } + + &.retweeted { + } +} + +.status:hover .animated.avatar { + canvas { + display: none; + } + img { + visibility: visible; + } +} + +.status { + display: flex; + padding: 0.6em; +} + +.status-conversation:last-child { + border-bottom: none; +} + +.muted { + padding: 0.25em 0.5em; + button { + margin-left: auto; } - .status-el { - hyphens: auto; - overflow-wrap: break-word; - word-wrap: break-word; - word-break: break-word; - border-left-width: 0px; - line-height: 18px; - - .timeline & { - border-bottom-width: 1px; - border-bottom-style: solid; - } - - .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 { - - } - - .expand { - margin-right: -0.3em; - } - - a { - display: inline-block; - word-break: break-all; - } - - .status-content { - margin: 3px 15px 4px 0; - max-height: 400px; - overflow-y: auto; - overflow-x: hidden; - - img, video { - max-width: 100%; - max-height: 400px; - vertical-align: middle; - object-fit: contain; - } - - blockquote { - margin: 0.2em 0 0.2em 2em; - font-style: italic; - } - } - - p { - margin: 0; - margin-top: 0.2em; - margin-bottom: 0.5em; - } - - .media-left { - margin: 0.2em 0.3em 0 0; - img { - float: right; - 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; - } - } - } - } - - .status-fadein { - animation-duration: 0.5s; - animation-name: fadein; - } - - @keyframes fadein { - from { - opacity: 0; - } - to { - opacity: 1; - } - } - - .greentext { - color: green; - } - - .status-conversation { - border-left-style: solid; - } - - .status-actions { - padding-top: 0.15em; - width: 100%; - display: flex; - - div, favorite-button { - max-width: 6em; - flex: 1; - } - } - - .icon-reply:hover { - color: $blue; - } - - .icon-reply.icon-reply-active { - color: $blue; - } - - .status .avatar { - 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 { - width: 32px; - } - - .status { - padding: 0.4em 0.7em 0.45em 0.7em; - border-left: 4px rgba(255, 48, 16, 0.65); - border-left-style: inherit; - } - - .status-conversation:last-child { - border-bottom: none; - } - - .timeline .panel.timeline { - border-radius: 10px; - overflow: hidden; - } - - .muted { - padding: 0.1em 0.4em 0.1em 0.8em; - button { - margin-left: auto; - } - - .muteWords { - margin-left: 10px; - } - } - - a.unmute { - display: block; - margin-left: auto; - } - - .reply-left { - flex: 0; - min-width: 48px; - } - - .reply-body { - 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; - } - } + .muteWords { + margin-left: 10px; + } +} + +a.unmute { + display: block; + margin-left: auto; +} + +.reply-left { + flex: 0; + min-width: 48px; +} + +.reply-body { + flex: 1; +} + +.timeline > { + .status-el:last-child { + border-bottom-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;; + border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); + } +} + +@media all and (max-width: 960px) { + .status-el { + .retweet-info { + .avatar { + margin-left: 20px; + } + } + } + .status { + max-width: 100%; + } + + .status .avatar { + width: 40px; + height: 40px; + } + + .status .avatar-compact { + width: 32px; + height: 32px; + } +} </style> |
