logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe
commit: 020d2f83f9fe2290bd74dd118f7e796a415439a4
parent: daa018aecf58599f4c5c0d2f401750e1ebe93cf2
Author: Shpuld Shpludson <shp@cock.li>
Date:   Wed, 11 Apr 2018 03:37:25 +0000

Merge branch 'feature/rewrite-status-and-notifications-and-preview' into 'develop'

Rewrite status and notifications and preview, graceful handling of linkless attachments

See merge request pleroma/pleroma-fe!223

Diffstat:

Msrc/App.scss5+++++
Msrc/components/attachment/attachment.js6+++++-
Msrc/components/attachment/attachment.vue168+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/components/conversation/conversation.vue2+-
Asrc/components/notification/notification.js24++++++++++++++++++++++++
Asrc/components/notification/notification.vue37+++++++++++++++++++++++++++++++++++++
Msrc/components/notifications/notifications.js7+++----
Msrc/components/notifications/notifications.scss224++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/components/notifications/notifications.vue43+------------------------------------------
Msrc/components/status/status.js35+++++++++++++++++++++++++++++++++--
Msrc/components/status/status.vue397+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/i18n/messages.js16++++++++++++----
12 files changed, 531 insertions(+), 433 deletions(-)

diff --git a/src/App.scss b/src/App.scss @@ -391,6 +391,11 @@ nav { } } +.faint { + color: $fallback--faint; + color: var(--faint, $fallback--faint); +} + @media all and (max-width: 959px) { .mobile-hidden { display: none; diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js @@ -6,7 +6,8 @@ const Attachment = { props: [ 'attachment', 'nsfw', - 'statusId' + 'statusId', + 'size' ], data () { return { @@ -29,6 +30,9 @@ const Attachment = { }, isEmpty () { return (this.type === 'html' && !this.attachment.oembed) || this.type === 'unknown' + }, + isSmall () { + return this.size === 'small' } }, methods: { diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue @@ -1,5 +1,8 @@ <template> - <div class="attachment" :class="{[type]: true, loading}" v-show="!isEmpty"> + <div v-if="size==='hide'"> + <a class="placeholder" v-if="type !== 'html'" target="_blank" :href="attachment.url">[{{nsfw ? "NSFW/" : ""}}{{type.toUpperCase()}}]</a> + </div> + <div v-else class="attachment" :class="{[type]: true, loading, 'small-attachment': isSmall}" v-show="!isEmpty"> <a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()"> <img :key="nsfwImage" :src="nsfwImage"/> </a> @@ -8,10 +11,10 @@ </div> <a v-if="type === 'image' && !hidden" class="image-attachment" :href="attachment.url" target="_blank"> - <StillImage referrerpolicy="no-referrer" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/> + <StillImage :class="{'small': isSmall}" referrerpolicy="no-referrer" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/> </a> - <video v-if="type === 'video' && !hidden" :src="attachment.url" controls loop></video> + <video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" :src="attachment.url" controls loop></video> <audio v-if="type === 'audio'" :src="attachment.url" controls></audio> @@ -41,110 +44,129 @@ flex: 0 0 auto; max-height: 300px; max-width: 100%; - line-height: 0; + } - video { - max-height: 300px; + .placeholder { + margin-right: 0.5em; + } + + .small-attachment { + &.image, &.video { + max-width: 35%; } + max-height: 100px; } .attachment { flex: 1 0 30%; margin: 0.5em 0.7em 0.6em 0.0em; align-self: flex-start; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); + line-height: 0; border-style: solid; border-width: 1px; border-radius: $fallback--attachmentRadius; border-radius: var(--attachmentRadius, $fallback--attachmentRadius); + border-color: $fallback--border; + border-color: var(--border, $fallback--border); overflow: hidden; + } + // fixes small gap below video + &.video { + line-height: 0; + } - // fixes small gap below video - &.video { - line-height: 0; - } + &.html { + flex-basis: 90%; + width: 100%; + display: flex; + } - &.html { - flex-basis: 90%; - width: 100%; - display: flex; - } + &.loading { + cursor: progress; + } - &.loading { - cursor: progress; - } + .hider { + position: absolute; + margin: 10px; + padding: 5px; + background: rgba(230,230,230,0.6); + font-weight: bold; + z-index: 4; + } - .hider { - position: absolute; - margin: 10px; - padding: 5px; - background: rgba(230,230,230,0.6); - font-weight: bold; - z-index: 4; - } + .small { + max-height: 100px; + } + video { + max-height: 500px; + height: 100%; + width: 100%; + z-index: 0; + } - video { - max-height: 500px; - height: 100%; - width: 100%; - z-index: 0; - } + audio { + width: 100%; + } - audio { - width: 100%; - } + img.media-upload { + line-height: 0; + max-height: 300px; + max-width: 100%; + } - img.media-upload { - margin-bottom: -2px; - max-height: 300px; - max-width: 100%; - } + .oembed { + width: 100%; + margin-right: 15px; + display: flex; - .oembed { + img { width: 100%; - margin-right: 15px; - display: flex; + } + .image { + flex: 1; img { - width: 100%; - } - - .image { - flex: 1; - img { - border: 0px; - border-radius: $fallback--attachmentRadius; - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); - height: 100%; - object-fit: cover; - } + border: 0px; + border-radius: 5px; + height: 100%; + object-fit: cover; } + } - .text { - flex: 2; - margin: 8px; - word-break: break-all; - h1 { - font-size: 14px; - margin: 0px; - } + .text { + flex: 2; + margin: 8px; + word-break: break-all; + h1 { + font-size: 14px; + margin: 0px; } } + } - a.image-attachment { - display: flex; - flex: 1; + .image-attachment { + display: flex; + flex: 1; + + .still-image { + width: 100%; + height: 100%; + } + .small { img { - object-fit: contain; - width: 100%; - height: 100%; /* If this isn't here, chrome will stretch the images */ - max-height: 500px; - image-orientation: from-image; + max-height: 100px; } } + + img { + object-fit: contain; + width: 100%; + height: 100%; /* If this isn't here, chrome will stretch the images */ + max-height: 500px; + image-orientation: from-image; + } } } </style> diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue @@ -8,7 +8,7 @@ </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" :replies="getReplies(status.id)"></status> + <status v-for="status in conversation" @goto="setHighlight" :key="status.id" :inlineExpanded="collapsable" :statusoid="status" :expandable='false' :focused="focused(status.id)" :inConversation='true' :highlight="highlight" :replies="getReplies(status.id)"></status> </div> </div> </div> diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js @@ -0,0 +1,24 @@ +import Status from '../status/status.vue' +import StillImage from '../still-image/still-image.vue' +import UserCardContent from '../user_card_content/user_card_content.vue' + +const Notification = { + data () { + return { + userExpanded: false + } + }, + props: [ + 'notification' + ], + components: { + Status, StillImage, UserCardContent + }, + methods: { + toggleUserExpanded () { + this.userExpanded = !this.userExpanded + } + } +} + +export default Notification diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue @@ -0,0 +1,37 @@ +<template> + <status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status> + <div class="non-mention" v-else> + <a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded"> + <StillImage class='avatar-compact' :src="notification.action.user.profile_image_url_original"/> + </a> + <div class='notification-right'> + <div class="usercard notification-usercard" v-if="userExpanded"> + <user-card-content :user="notification.action.user" :switcher="false"></user-card-content> + </div> + <span class="notification-details"> + <div class="name-and-action"> + <span class="username" :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span> + <span v-if="notification.type === 'favorite'"> + <i class="fa icon-star lit"></i> + <small>{{$t('notifications.favorited_you')}}</small> + </span> + <span v-if="notification.type === 'repeat'"> + <i class="fa icon-retweet lit"></i> + <small>{{$t('notifications.repeated_you')}}</small> + </span> + <span v-if="notification.type === 'follow'"> + <i class="fa icon-user-plus lit"></i> + <small>{{$t('notifications.followed_you')}}</small> + </span> + </div> + <small class="timeago"><router-link :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small> + </span> + <div class="follow-text" v-if="notification.type === 'follow'"> + <router-link :to="{ name: 'user-profile', params: { id: notification.action.user.id } }">@{{notification.action.user.screen_name}}</router-link> + </div> + <status v-else class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status> + </div> + </div> +</template> + +<script src="./notification.js"></script> diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js @@ -1,12 +1,11 @@ -import Status from '../status/status.vue' -import StillImage from '../still-image/still-image.vue' +import Notification from '../notification/notification.vue' import { sortBy, take, filter } from 'lodash' const Notifications = { data () { return { - visibleNotificationCount: 10 + visibleNotificationCount: 20 } }, computed: { @@ -27,7 +26,7 @@ const Notifications = { } }, components: { - Status, StillImage + Notification }, watch: { unseenCount (count) { diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss @@ -46,129 +46,167 @@ font-size: 0.9em; text-align: center; line-height: 1.3em; - padding: 1px; } - .notification { - // Will have to use pixels here to ensure consistent distance with - // pad alone and pad + border, browsers bad at rounding this with em, - // they love to give a 1 pixel ghost offset with 0.7em vs 0.3em + 0.4em, - // which does not happen with 10px vs 4px + 6px. - padding: 0.4em 0 0 10px; - display: flex; - border-bottom: 1px solid; - border-bottom-color: inherit; + .unseen { + border-left: 4px solid $fallback--cRed; + border-left: 4px solid var(--cRed, $fallback--cRed); + padding-left: 0; + } +} - .notification-gradient { - 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%) +.notification { + box-sizing: border-box; + display: flex; + border-bottom: 1px solid; + border-bottom-color: inherit; + padding-left: 4px; + + .avatar-compact { + width: 32px; + height: 32px; + border-radius: $fallback--avatarAltRadius; + border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + overflow: hidden; + line-height: 0; + + &.animated::before { + display: none; } + } - time { - white-space: nowrap; + &:hover .animated.avatar { + canvas { + display: none; } + img { + visibility: visible; + } + } - .text { - min-width: 0px; - word-wrap: break-word; - line-height:18px; - position: relative; - overflow: hidden; + .notification-usercard { + margin: 0; + } - .icon-retweet.lit { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); + .non-mention { + display: flex; + flex: 1; + flex-wrap: nowrap; + padding: 0.6em; + min-width: 0; + .avatar-container { + width: 32px; + height: 32px; + } + .status-el { + .status { + padding: 0.25em 0; + color: $fallback--faint; + color: var($fallback--faint, --faint); } - - .icon-user-plus.lit { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); + padding: 0; + .status-content.media-body { + margin: 0; } + } + } - .icon-reply.lit { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); - } + .follow-text { + padding: 0.5em 0; + } - .icon-star.lit { - color: orange; - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); - } + .status-el { + flex: 1; + } - .status-content { - margin: 0; - max-height: 300px; - } + time { + white-space: nowrap; + } - h1 { - word-break: break-all; - margin: 0 0 0.3em; - padding: 0; - font-size: 1em; - line-height:20px; - small { - font-weight: lighter; - } - } + .notification-right { + flex: 1; + padding-left: 0.8em; + min-width: 0; + } - padding: 0.3em 0.8em 0.5em; - p { - margin: 0; - margin-top: 0; - margin-bottom: 0.3em; - } + .notification-details { + min-width: 0px; + word-wrap: break-word; + line-height:18px; + position: relative; + overflow: hidden; + width: 100%; + flex: 1 1 0; + display: flex; + flex-wrap: nowrap; + + .name-and-action { + flex: 1; } - .avatar { - margin-top: 0.3em; - width: 32px; - height: 32px; - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); - overflow: hidden; - line-height: 0; + .username { + font-weight: bolder; + } + .timeago { + float: right; + font-size: 12px; + } - &.animated::before { - display: none; - } + .icon-retweet.lit { + color: $fallback--cGreen; + color: var(--cGreen, $fallback--cGreen); + } + .icon-user-plus.lit { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); } - &:hover .animated.avatar { - canvas { - display: none; - } - img { - visibility: visible; - } + .icon-reply.lit { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); } - &:last-child { - border-bottom: none; + .icon-star.lit { + color: orange; + color: $fallback--cOrange; + color: var(--cOrange, $fallback--cOrange); } - } - .notification-content { - max-height: 12em; - overflow-y: hidden; - //text-overflow: ellipsis; + .status-content { + margin: 0; + max-height: 300px; + } - img { - object-fit: contain; + h1 { + word-break: break-all; + margin: 0 0 0.3em; + padding: 0; + font-size: 1em; + line-height:20px; + small { + font-weight: lighter; + } + } + + p { + margin: 0; + margin-top: 0; + margin-bottom: 0.3em; } } - .notification-gradient { - position: absolute; - width: 100%; - height: 4em; - margin-top:8em; + &:last-child { + border-bottom: none; } +} - .unseen { - border-left: 4px solid $fallback--cRed; - border-left: 4px solid var(--cRed, $fallback--cRed); - padding-left: 6px; +.notification-content { + max-height: 12em; + overflow-y: hidden; + //text-overflow: ellipsis; + + img { + object-fit: contain; } } diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue @@ -8,48 +8,7 @@ </div> <div class="panel-body"> <div v-for="notification in visibleNotifications" :key="notification" class="notification" :class='{"unseen": !notification.seen}'> - <div> - <a :href="notification.action.user.statusnet_profile_url" target="_blank"> - <StillImage class='avatar' :src="notification.action.user.profile_image_url_original"/> - </a> - </div> - <div class='text' style="width: 100%;"> - <div v-if="notification.type === 'favorite'"> - <h1> - <span :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span> - <i class="fa icon-star 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 class="notification-gradient"></div> - <div class="notification-content" v-html="notification.status.statusnet_html"></div> - </div> - <div v-if="notification.type === 'repeat'"> - <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 class="notification-gradient"></div> - <div class="notification-content" v-html="notification.status.statusnet_html"></div> - </div> - <div v-if="notification.type === 'mention'"> - <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 v-if="notification.type === 'follow'"> - <h1> - <span :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span> - <i class="fa icon-user-plus lit"></i> - </h1> - <div> - <router-link :to="{ name: 'user-profile', params: { id: notification.action.user.id } }">@{{ notification.action.user.screen_name }}</router-link> {{$t('notifications.followed_you')}} - </div> - </div> - </div> + <notification :notification="notification"></notification> </div> </div> </div> diff --git a/src/components/status/status.js b/src/components/status/status.js @@ -8,6 +8,7 @@ import StillImage from '../still-image/still-image.vue' import { filter, find } from 'lodash' const Status = { + name: 'Status', props: [ 'statusoid', 'expandable', @@ -15,7 +16,10 @@ const Status = { 'focused', 'highlight', 'compact', - 'replies' + 'replies', + 'noReplyLinks', + 'noHeading', + 'inlineExpanded' ], data: () => ({ replying: false, @@ -23,7 +27,8 @@ const Status = { unmuted: false, userExpanded: false, preview: null, - showPreview: false + showPreview: false, + showingTall: false }), computed: { muteWords () { @@ -64,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: { @@ -102,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 @@ -1,123 +1,98 @@ <template> - <div class="status-el" 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="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 status-fadein" v-else-if="!status.deleted" v-bind:class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inConversation }]" > - <template v-if="muted"> + <div class="status-el status-fadein" :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="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"> - <StillImage @click.native.prevent="toggleUserExpanded" :class="{retweeted: retweet}" class='avatar' :src="status.user.profile_image_url_original"/> - <StillImage v-if="retweet" class='avatar avatar-retweeter' :src="statusoid.user.profile_image_url_original"/> + + <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="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"> &gt; + <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"> &gt; <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}}&nbsp;</a> - </small> - </h4> - </div> - <div class="heading-icons"> - <a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="icon-eye-off"></i></a> - <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="icon-binoculars"></i></a> - <template v-if="expandable"> - <a href="#" @click.prevent="toggleExpanded" class="expand"><i class="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}}&nbsp;</a> + </small> + </h4> </div> - - <div class="status-preview" v-if="showPreview && preview"> - <StillImage 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-binoculars"></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" v-else-if="showPreview"> + </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 base00-background base03-border" 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="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 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,18 +101,29 @@ </template> <script src="./status.js" ></script> - <style lang="scss"> @import '../../_variables.scss'; -status-text-container { - display: block; +.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: 34em; - padding: 0.5em; + max-width: 95%; display: flex; background-color: $fallback--bg; background-color: var(--bg, $fallback--bg); @@ -148,26 +134,13 @@ status-text-container { border-radius: $fallback--tooltipRadius; border-radius: var(--tooltipRadius, $fallback--tooltipRadius); box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5); - margin-top: 0.5em; - margin-left: 1em; + margin-top: 0.25em; + margin-left: 0.5em; z-index: 50; - - .avatar { - flex-shrink: 0; - width: 32px; - height: 32px; - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); - } - - .text { - h4 { - margin-bottom: 0.4em; - small { - font-weight: lighter; - } - } - padding: 0 0.5em 0.5em 0.5em; + .status { + flex: 1; + border: 0; + min-width: 15em; } } @@ -175,7 +148,10 @@ status-text-container { display: block; font-size: 2em; min-width: 8em; + padding: 0.5em; text-align: center; + border-width: 1px; + border-style: solid; } .status-el { @@ -185,6 +161,7 @@ status-text-container { 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; @@ -195,70 +172,67 @@ status-text-container { background-color: var(--lightBg, $fallback--lightBg); } - .usercard { - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - } - .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; + padding: 0; + margin: 0 0 0.25em 0.8em; } .media-heading { - display: flex; - min-height: 1.4em; - margin-bottom: 0.3em; - - .links a i { - color: $fallback--link; - color: var(--link, $fallback--link); - } + flex-wrap: nowrap; + } + .media-heading-left { + padding: 0; + vertical-align: bottom; + flex-basis: 100%; small { font-weight: lighter; } - h4 { - margin-right: 0.4em; + font-size: 14px; + margin-right: 0.25em; } - .name-and-links { + padding: 0; flex: 1 0; display: flex; flex-wrap: wrap; } - + .links a { + padding-top: 1px; + font-size: 12px; + color: $fallback--link; + color: var(--link, $fallback--link); + } .replies { - flex-basis: 100%; + line-height: 16px; + } + .reply-link { + margin-right: 0.2em; } } - .source_url { - - } - - .expand { - margin-right: -0.3em; + .media-heading-right { + flex-shrink: 0; + display: flex; + 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 { @@ -266,12 +240,35 @@ status-text-container { word-break: break-all; } - .status-content { - margin: 3px 15px 4px 0; - max-height: 400px; - overflow-y: auto; + .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; @@ -283,43 +280,34 @@ status-text-container { 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; - .avatar { - float: right; - border-radius: $fallback--avatarRadius; - border-radius: var(--avatarRadius, $fallback--avatarRadius); + p { + margin: 0; + margin-top: 0.2em; + margin-bottom: 0.5em; } } .retweet-info { - padding: 0.7em 0 0 0.6em; - - .media-left { - display: flex; + padding: 0.3em 0.6em 0 0.6em; + margin: 0 0 -0.3em 0; + .avatar { + border-radius: $fallback--avatarAltRadius; + border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + margin-left: 28px; + width: 20px; + height: 20px; + } - i { - align-self: center; - text-align: right; - flex: 1; - padding-right: 0.3em; - } + .media-body { + font-size: 1em; + line-height: 22px; } } - - } .status-fadein { - animation-duration: 0.5s; + animation-duration: 0.3s; animation-name: fadein; } @@ -327,7 +315,6 @@ status-text-container { from { opacity: 0; } - to { opacity: 1; } @@ -342,11 +329,11 @@ status-text-container { } .status-actions { - padding-top: 0.15em; width: 100%; display: flex; div, favorite-button { + padding-top: 0.25em; max-width: 6em; flex: 1; } @@ -362,12 +349,20 @@ status-text-container { color: var(--cBlue, $fallback--cBlue); } -.status .avatar { +.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%; @@ -379,10 +374,6 @@ status-text-container { } &.retweeted { - width: 40px; - height: 40px; - margin-right: 8px; - margin-bottom: 8px; } } @@ -395,20 +386,9 @@ status-text-container { } } -.status .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; + display: flex; + padding: 0.6em; border-left: 4px $fallback--cRed; border-left: 4px var(--cRed, $fallback--cRed); border-left-style: inherit; @@ -423,7 +403,7 @@ status-text-container { } .muted { - padding: 0.1em 0.4em 0.1em 0.8em; + padding: 0.25em 0.5em; button { margin-left: auto; } @@ -449,11 +429,12 @@ a.unmute { @media all and (max-width: 960px) { .status-el { - .name-and-links { - margin-left: -0.25em; + .retweet-info { + .avatar { + margin-left: 20px; + } } } - .status { max-width: 100%; } @@ -461,21 +442,11 @@ a.unmute { .status .avatar { width: 40px; height: 40px; - - &.retweeted { - width: 34px; - height: 34px; - margin-right: 8px; - margin-bottom: 8px; - } } - .status .avatar-retweeter { - width: 22px; - height: 22px; - position: absolute; - margin-left: 18px; - margin-top: 18px; + .status .avatar-compact { + width: 32px; + height: 32px; } } diff --git a/src/i18n/messages.js b/src/i18n/messages.js @@ -124,7 +124,9 @@ const fi = { error_fetching: 'Virhe ladatessa viestejä', up_to_date: 'Ajantasalla', load_older: 'Lataa vanhempia viestejä', - conversation: 'Keskustelu' + conversation: 'Keskustelu', + collapse: 'Sulje', + repeated: 'toisti' }, settings: { user_settings: 'Käyttäjän asetukset', @@ -160,7 +162,9 @@ const fi = { notifications: { notifications: 'Ilmoitukset', read: 'Lue!', - followed_you: 'seuraa sinua' + followed_you: 'seuraa sinua', + favorited_you: 'tykkäsi viestistäsi', + repeated_you: 'toisti viestisi' }, login: { login: 'Kirjaudu sisään', @@ -220,7 +224,9 @@ const en = { error_fetching: 'Error fetching updates', up_to_date: 'Up-to-date', load_older: 'Load older statuses', - conversation: 'Conversation' + conversation: 'Conversation', + collapse: 'Collapse', + repeated: 'repeated' }, settings: { user_settings: 'User Settings', @@ -272,7 +278,9 @@ const en = { notifications: { notifications: 'Notifications', read: 'Read!', - followed_you: 'followed you' + followed_you: 'followed you', + favorited_you: 'favorited your status', + repeated_you: 'repeated your status' }, login: { login: 'Log in',