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:
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"> >
+ <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"> >
<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="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}} </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',