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
This commit is contained in:
Shpuld Shpludson 2018-04-11 03:37:25 +00:00
commit 020d2f83f9
12 changed files with 568 additions and 470 deletions

View file

@ -391,6 +391,11 @@ nav {
} }
} }
.faint {
color: $fallback--faint;
color: var(--faint, $fallback--faint);
}
@media all and (max-width: 959px) { @media all and (max-width: 959px) {
.mobile-hidden { .mobile-hidden {
display: none; display: none;

View file

@ -6,7 +6,8 @@ const Attachment = {
props: [ props: [
'attachment', 'attachment',
'nsfw', 'nsfw',
'statusId' 'statusId',
'size'
], ],
data () { data () {
return { return {
@ -29,6 +30,9 @@ const Attachment = {
}, },
isEmpty () { isEmpty () {
return (this.type === 'html' && !this.attachment.oembed) || this.type === 'unknown' return (this.type === 'html' && !this.attachment.oembed) || this.type === 'unknown'
},
isSmall () {
return this.size === 'small'
} }
}, },
methods: { methods: {

View file

@ -1,5 +1,8 @@
<template> <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()"> <a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()">
<img :key="nsfwImage" :src="nsfwImage"/> <img :key="nsfwImage" :src="nsfwImage"/>
</a> </a>
@ -8,10 +11,10 @@
</div> </div>
<a v-if="type === 'image' && !hidden" class="image-attachment" :href="attachment.url" target="_blank"> <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> </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> <audio v-if="type === 'audio'" :src="attachment.url" controls></audio>
@ -41,110 +44,129 @@
flex: 0 0 auto; flex: 0 0 auto;
max-height: 300px; max-height: 300px;
max-width: 100%; max-width: 100%;
line-height: 0; }
video { .placeholder {
max-height: 300px; margin-right: 0.5em;
}
.small-attachment {
&.image, &.video {
max-width: 35%;
} }
max-height: 100px;
} }
.attachment { .attachment {
flex: 1 0 30%; flex: 1 0 30%;
margin: 0.5em 0.7em 0.6em 0.0em; margin: 0.5em 0.7em 0.6em 0.0em;
align-self: flex-start; align-self: flex-start;
border-color: $fallback--border; line-height: 0;
border-color: var(--border, $fallback--border);
border-style: solid; border-style: solid;
border-width: 1px; border-width: 1px;
border-radius: $fallback--attachmentRadius; border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius); border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
overflow: hidden; overflow: hidden;
}
// fixes small gap below video
&.video {
line-height: 0;
}
// fixes small gap below video &.html {
&.video { flex-basis: 90%;
line-height: 0; width: 100%;
} display: flex;
}
&.html { &.loading {
flex-basis: 90%; cursor: progress;
width: 100%; }
display: flex;
}
&.loading { .hider {
cursor: progress; position: absolute;
} margin: 10px;
padding: 5px;
background: rgba(230,230,230,0.6);
font-weight: bold;
z-index: 4;
}
.hider { .small {
position: absolute; max-height: 100px;
margin: 10px; }
padding: 5px; video {
background: rgba(230,230,230,0.6); max-height: 500px;
font-weight: bold; height: 100%;
z-index: 4; width: 100%;
} z-index: 0;
}
video { audio {
max-height: 500px; width: 100%;
height: 100%; }
width: 100%;
z-index: 0;
}
audio { img.media-upload {
line-height: 0;
max-height: 300px;
max-width: 100%;
}
.oembed {
width: 100%;
margin-right: 15px;
display: flex;
img {
width: 100%; width: 100%;
} }
img.media-upload { .image {
margin-bottom: -2px;
max-height: 300px;
max-width: 100%;
}
.oembed {
width: 100%;
margin-right: 15px;
display: flex;
img {
width: 100%;
}
.image {
flex: 1;
img {
border: 0px;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
height: 100%;
object-fit: cover;
}
}
.text {
flex: 2;
margin: 8px;
word-break: break-all;
h1 {
font-size: 14px;
margin: 0px;
}
}
}
a.image-attachment {
display: flex;
flex: 1; flex: 1;
img { img {
object-fit: contain; border: 0px;
width: 100%; border-radius: 5px;
height: 100%; /* If this isn't here, chrome will stretch the images */ height: 100%;
max-height: 500px; object-fit: cover;
image-orientation: from-image;
} }
} }
.text {
flex: 2;
margin: 8px;
word-break: break-all;
h1 {
font-size: 14px;
margin: 0px;
}
}
}
.image-attachment {
display: flex;
flex: 1;
.still-image {
width: 100%;
height: 100%;
}
.small {
img {
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> </style>

View file

@ -8,7 +8,7 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div class="timeline"> <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> </div>
</div> </div>

View file

@ -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

View file

@ -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>

View file

@ -1,12 +1,11 @@
import Status from '../status/status.vue' import Notification from '../notification/notification.vue'
import StillImage from '../still-image/still-image.vue'
import { sortBy, take, filter } from 'lodash' import { sortBy, take, filter } from 'lodash'
const Notifications = { const Notifications = {
data () { data () {
return { return {
visibleNotificationCount: 10 visibleNotificationCount: 20
} }
}, },
computed: { computed: {
@ -27,7 +26,7 @@ const Notifications = {
} }
}, },
components: { components: {
Status, StillImage Notification
}, },
watch: { watch: {
unseenCount (count) { unseenCount (count) {

View file

@ -46,129 +46,167 @@
font-size: 0.9em; font-size: 0.9em;
text-align: center; text-align: center;
line-height: 1.3em; 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;
.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%)
}
time {
white-space: nowrap;
}
.text {
min-width: 0px;
word-wrap: break-word;
line-height:18px;
position: relative;
overflow: hidden;
.icon-retweet.lit {
color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen);
}
.icon-user-plus.lit {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.icon-reply.lit {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.icon-star.lit {
color: orange;
color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange);
}
.status-content {
margin: 0;
max-height: 300px;
}
h1 {
word-break: break-all;
margin: 0 0 0.3em;
padding: 0;
font-size: 1em;
line-height:20px;
small {
font-weight: lighter;
}
}
padding: 0.3em 0.8em 0.5em;
p {
margin: 0;
margin-top: 0;
margin-bottom: 0.3em;
}
}
.avatar {
margin-top: 0.3em;
width: 32px;
height: 32px;
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
overflow: hidden;
line-height: 0;
&.animated::before {
display: none;
}
}
&:hover .animated.avatar {
canvas {
display: none;
}
img {
visibility: visible;
}
}
&:last-child {
border-bottom: none;
}
}
.notification-content {
max-height: 12em;
overflow-y: hidden;
//text-overflow: ellipsis;
img {
object-fit: contain;
}
}
.notification-gradient {
position: absolute;
width: 100%;
height: 4em;
margin-top:8em;
} }
.unseen { .unseen {
border-left: 4px solid $fallback--cRed; border-left: 4px solid $fallback--cRed;
border-left: 4px solid var(--cRed, $fallback--cRed); border-left: 4px solid var(--cRed, $fallback--cRed);
padding-left: 6px; padding-left: 0;
}
}
.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;
}
}
&:hover .animated.avatar {
canvas {
display: none;
}
img {
visibility: visible;
}
}
.notification-usercard {
margin: 0;
}
.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);
}
padding: 0;
.status-content.media-body {
margin: 0;
}
}
}
.follow-text {
padding: 0.5em 0;
}
.status-el {
flex: 1;
}
time {
white-space: nowrap;
}
.notification-right {
flex: 1;
padding-left: 0.8em;
min-width: 0;
}
.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;
}
.username {
font-weight: bolder;
}
.timeago {
float: right;
font-size: 12px;
}
.icon-retweet.lit {
color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen);
}
.icon-user-plus.lit {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.icon-reply.lit {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.icon-star.lit {
color: orange;
color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange);
}
.status-content {
margin: 0;
max-height: 300px;
}
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;
}
}
&:last-child {
border-bottom: none;
}
}
.notification-content {
max-height: 12em;
overflow-y: hidden;
//text-overflow: ellipsis;
img {
object-fit: contain;
} }
} }

View file

@ -8,48 +8,7 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div v-for="notification in visibleNotifications" :key="notification" class="notification" :class='{"unseen": !notification.seen}'> <div v-for="notification in visibleNotifications" :key="notification" class="notification" :class='{"unseen": !notification.seen}'>
<div> <notification :notification="notification"></notification>
<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>
</div> </div>
</div> </div>
</div> </div>

View file

@ -8,6 +8,7 @@ import StillImage from '../still-image/still-image.vue'
import { filter, find } from 'lodash' import { filter, find } from 'lodash'
const Status = { const Status = {
name: 'Status',
props: [ props: [
'statusoid', 'statusoid',
'expandable', 'expandable',
@ -15,7 +16,10 @@ const Status = {
'focused', 'focused',
'highlight', 'highlight',
'compact', 'compact',
'replies' 'replies',
'noReplyLinks',
'noHeading',
'inlineExpanded'
], ],
data: () => ({ data: () => ({
replying: false, replying: false,
@ -23,7 +27,8 @@ const Status = {
unmuted: false, unmuted: false,
userExpanded: false, userExpanded: false,
preview: null, preview: null,
showPreview: false showPreview: false,
showingTall: false
}), }),
computed: { computed: {
muteWords () { muteWords () {
@ -64,6 +69,29 @@ const Status = {
} }
// use conversation highlight only when in conversation // use conversation highlight only when in conversation
return this.status.id === this.highlight 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: { components: {
@ -102,6 +130,9 @@ const Status = {
toggleUserExpanded () { toggleUserExpanded () {
this.userExpanded = !this.userExpanded this.userExpanded = !this.userExpanded
}, },
toggleShowTall () {
this.showingTall = !this.showingTall
},
replyEnter (id, event) { replyEnter (id, event) {
this.showPreview = true this.showPreview = true
const targetId = Number(id) const targetId = Number(id)

View file

@ -1,123 +1,98 @@
<template> <template>
<div class="status-el" v-if="compact"> <div class="status-el status-fadein" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
<div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></div> <template v-if="muted && !noReplyLinks">
<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="media status container muted"> <div class="media status container muted">
<small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small> <small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
<small class="muteWords">{{muteWordHits.join(', ')}}</small> <small class="muteWords">{{muteWordHits.join(', ')}}</small>
<a href="#" class="unmute" @click.prevent="toggleMute"><i class="icon-eye-off"></i></a> <a href="#" class="unmute" @click.prevent="toggleMute"><i class="icon-eye-off"></i></a>
</div> </div>
</template> </template>
<template v-if="!muted"> <template v-else>
<div v-if="retweet" class="media container retweet-info"> <div v-if="retweet && !noHeading" class="media container retweet-info">
<div class="media-left"> <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> <i class='fa icon-retweet retweeted'></i>
</div> {{$t('timeline.repeated')}}
<div class="media-body">
Repeated by <a :href="statusoid.user.statusnet_profile_url" style="font-weight: bold;" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
</div> </div>
</div> </div>
<div class="media status container">
<div class="media-left"> <div class="media status">
<a :href="status.user.statusnet_profile_url"> <div v-if="!noHeading" class="media-left">
<StillImage @click.native.prevent="toggleUserExpanded" :class="{retweeted: retweet}" class='avatar' :src="status.user.profile_image_url_original"/> <a :href="status.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
<StillImage v-if="retweet" class='avatar avatar-retweeter' :src="statusoid.user.profile_image_url_original"/> <StillImage class='avatar' :class="{'avatar-compact': compact}" :src="status.user.profile_image_url_original"/>
</a> </a>
</div> </div>
<div class="media-body"> <div class="status-body">
<div class="usercard" v-if="userExpanded"> <div class="usercard media-body" v-if="userExpanded">
<user-card-content :user="status.user" :switcher="false"></user-card-content> <user-card-content :user="status.user" :switcher="false"></user-card-content>
</div> </div>
<div class="user-content"> <div v-if="!noHeading" class="media-body container media-heading">
<div class="media-heading"> <div class="media-heading-left">
<div class="name-and-links"> <div class="name-and-links">
<h4 class="user-name">{{status.user.name}}</h4> <h4 class="user-name">{{status.user.name}}</h4>
<div class="links"> <span class="links">
<h4> <router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link>
<small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small> <span v-if="status.in_reply_to_screen_name"> &gt;
<small v-if="status.in_reply_to_screen_name"> &gt;
<router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }"> <router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }">
{{status.in_reply_to_screen_name}} {{status.in_reply_to_screen_name}}
</router-link> </router-link>
</small> </span>
<template v-if="isReply"> <a v-if="isReply && !noReplyLinks" href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)">
<small> <i class="icon-reply" @mouseenter="replyEnter(status.in_reply_to_status_id, $event)" @mouseout="replyLeave()"></i>
<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> </a>
</small> </span>
</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>
</div> </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>
<div class="media-heading-right">
<div class="status-preview" v-if="showPreview && preview"> <router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }">
<StillImage class="avatar" :src="preview.user.profile_image_url_original"/> <timeago :since="status.created_at" :auto-update="60"></timeago>
<div class="text"> </router-link>
<h4> <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="icon-binoculars"></i></a>
{{ preview.user.name }} <template v-if="expandable">
<small><a>{{ preview.user.screen_name}}</a></small> <a href="#" @click.prevent="toggleExpanded"><i class="icon-plus-squared"></i></a>
</h4> </template>
<div @click.prevent="linkClicked" class="status-content" v-html="preview.statusnet_html"></div> <a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="icon-eye-off"></i></a>
</div>
</div>
<div class="status-preview status-preview-loading" v-else-if="showPreview">
<i class="icon-spin4 animate-spin"></i>
</div>
<div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></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>
</div> </div>
<div class='status-actions'> <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 :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 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 v-if="!noHeading && !noReplyLinks" class='status-actions media-body'>
<div v-if="loggedIn"> <div v-if="loggedIn">
<a href="#" v-on:click.prevent="toggleReplying"> <a href="#" v-on:click.prevent="toggleReplying">
<i class="icon-reply" :class="{'icon-reply-active': replying}"></i> <i class="icon-reply" :class="{'icon-reply-active': replying}"></i>
</a> </a>
</div> </div>
<retweet-button :loggedIn="loggedIn" :status=status></retweet-button> <retweet-button :loggedIn='loggedIn' :status='status'></retweet-button>
<favorite-button :loggedIn="loggedIn" :status=status></favorite-button> <favorite-button :loggedIn='loggedIn' :status='status'></favorite-button>
<delete-button :status=status></delete-button> <delete-button :status='status'></delete-button>
</div> </div>
</div> </div>
</div> </div>
<div class="status container" v-if="replying"> <div class="container" v-if="replying">
<div class="reply-left"/> <div class="reply-left"/>
<post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" v-on:posted="toggleReplying"/> <post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" v-on:posted="toggleReplying"/>
</div> </div>
@ -126,18 +101,29 @@
</template> </template>
<script src="./status.js" ></script> <script src="./status.js" ></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
status-text-container { .status-body {
display: block; 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 { .status-preview {
position: absolute; position: absolute;
max-width: 34em; max-width: 95%;
padding: 0.5em;
display: flex; display: flex;
background-color: $fallback--bg; background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg); background-color: var(--bg, $fallback--bg);
@ -148,26 +134,13 @@ status-text-container {
border-radius: $fallback--tooltipRadius; border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius); border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5); box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
margin-top: 0.5em; margin-top: 0.25em;
margin-left: 1em; margin-left: 0.5em;
z-index: 50; z-index: 50;
.status {
.avatar { flex: 1;
flex-shrink: 0; border: 0;
width: 32px; min-width: 15em;
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;
} }
} }
@ -175,7 +148,10 @@ status-text-container {
display: block; display: block;
font-size: 2em; font-size: 2em;
min-width: 8em; min-width: 8em;
padding: 0.5em;
text-align: center; text-align: center;
border-width: 1px;
border-style: solid;
} }
.status-el { .status-el {
@ -185,6 +161,7 @@ status-text-container {
word-break: break-word; word-break: break-word;
border-left-width: 0px; border-left-width: 0px;
line-height: 18px; line-height: 18px;
min-width: 0;
background-color: $fallback--bg; background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg); background-color: var(--bg, $fallback--bg);
border-color: $fallback--border; border-color: $fallback--border;
@ -195,70 +172,67 @@ status-text-container {
background-color: var(--lightBg, $fallback--lightBg); background-color: var(--lightBg, $fallback--lightBg);
} }
.usercard {
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
}
.timeline & { .timeline & {
border-bottom-width: 1px; border-bottom-width: 1px;
border-bottom-style: solid; border-bottom-style: solid;
} }
.notify {
.avatar {
border-width: 3px;
border-style: solid;
}
}
.media-body { .media-body {
flex: 1; flex: 1;
padding-left: 0.5em; padding: 0;
} margin: 0 0 0.25em 0.8em;
.user-content {
min-height: 52px;
padding-top: 1px;
} }
.media-heading { .media-heading {
display: flex; flex-wrap: nowrap;
min-height: 1.4em; }
margin-bottom: 0.3em;
.links a i {
color: $fallback--link;
color: var(--link, $fallback--link);
}
.media-heading-left {
padding: 0;
vertical-align: bottom;
flex-basis: 100%;
small { small {
font-weight: lighter; font-weight: lighter;
} }
h4 { h4 {
margin-right: 0.4em; font-size: 14px;
margin-right: 0.25em;
} }
.name-and-links { .name-and-links {
padding: 0;
flex: 1 0; flex: 1 0;
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
} }
.links a {
padding-top: 1px;
font-size: 12px;
color: $fallback--link;
color: var(--link, $fallback--link);
}
.replies { .replies {
flex-basis: 100%; line-height: 16px;
}
.reply-link {
margin-right: 0.2em;
} }
} }
.source_url { .media-heading-right {
flex-shrink: 0;
} display: flex;
flex-wrap: nowrap;
.expand { max-height: 1.5em;
margin-right: -0.3em; margin-left: 0.25em;
.timeago {
margin-right: 0.2em;
font-size: 12px;
padding-top: 1px;
}
i {
margin-left: 0.2em;
}
} }
a { a {
@ -266,12 +240,35 @@ status-text-container {
word-break: break-all; word-break: break-all;
} }
.status-content { .tall-status {
margin: 3px 15px 4px 0; position: relative;
max-height: 400px; height: 220px;
overflow-y: auto;
overflow-x: hidden; 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 { img, video {
max-width: 100%; max-width: 100%;
max-height: 400px; max-height: 400px;
@ -283,43 +280,34 @@ status-text-container {
margin: 0.2em 0 0.2em 2em; margin: 0.2em 0 0.2em 2em;
font-style: italic; font-style: italic;
} }
}
p { p {
margin: 0; margin: 0;
margin-top: 0.2em; margin-top: 0.2em;
margin-bottom: 0.5em; 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);
} }
} }
.retweet-info { .retweet-info {
padding: 0.7em 0 0 0.6em; 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;
}
.media-left { .media-body {
display: flex; font-size: 1em;
line-height: 22px;
i {
align-self: center;
text-align: right;
flex: 1;
padding-right: 0.3em;
}
} }
} }
} }
.status-fadein { .status-fadein {
animation-duration: 0.5s; animation-duration: 0.3s;
animation-name: fadein; animation-name: fadein;
} }
@ -327,7 +315,6 @@ status-text-container {
from { from {
opacity: 0; opacity: 0;
} }
to { to {
opacity: 1; opacity: 1;
} }
@ -342,11 +329,11 @@ status-text-container {
} }
.status-actions { .status-actions {
padding-top: 0.15em;
width: 100%; width: 100%;
display: flex; display: flex;
div, favorite-button { div, favorite-button {
padding-top: 0.25em;
max-width: 6em; max-width: 6em;
flex: 1; flex: 1;
} }
@ -362,12 +349,20 @@ status-text-container {
color: var(--cBlue, $fallback--cBlue); 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; width: 48px;
height: 48px; height: 48px;
border-radius: $fallback--avatarRadius; border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius); border-radius: var(--avatarRadius, $fallback--avatarRadius);
overflow: hidden; overflow: hidden;
position: relative;
img { img {
width: 100%; width: 100%;
@ -379,10 +374,6 @@ status-text-container {
} }
&.retweeted { &.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 { .status {
padding: 0.4em 0.7em 0.45em 0.7em; display: flex;
padding: 0.6em;
border-left: 4px $fallback--cRed; border-left: 4px $fallback--cRed;
border-left: 4px var(--cRed, $fallback--cRed); border-left: 4px var(--cRed, $fallback--cRed);
border-left-style: inherit; border-left-style: inherit;
@ -423,7 +403,7 @@ status-text-container {
} }
.muted { .muted {
padding: 0.1em 0.4em 0.1em 0.8em; padding: 0.25em 0.5em;
button { button {
margin-left: auto; margin-left: auto;
} }
@ -449,11 +429,12 @@ a.unmute {
@media all and (max-width: 960px) { @media all and (max-width: 960px) {
.status-el { .status-el {
.name-and-links { .retweet-info {
margin-left: -0.25em; .avatar {
margin-left: 20px;
}
} }
} }
.status { .status {
max-width: 100%; max-width: 100%;
} }
@ -461,21 +442,11 @@ a.unmute {
.status .avatar { .status .avatar {
width: 40px; width: 40px;
height: 40px; height: 40px;
&.retweeted {
width: 34px;
height: 34px;
margin-right: 8px;
margin-bottom: 8px;
}
} }
.status .avatar-retweeter { .status .avatar-compact {
width: 22px; width: 32px;
height: 22px; height: 32px;
position: absolute;
margin-left: 18px;
margin-top: 18px;
} }
} }

View file

@ -124,7 +124,9 @@ const fi = {
error_fetching: 'Virhe ladatessa viestejä', error_fetching: 'Virhe ladatessa viestejä',
up_to_date: 'Ajantasalla', up_to_date: 'Ajantasalla',
load_older: 'Lataa vanhempia viestejä', load_older: 'Lataa vanhempia viestejä',
conversation: 'Keskustelu' conversation: 'Keskustelu',
collapse: 'Sulje',
repeated: 'toisti'
}, },
settings: { settings: {
user_settings: 'Käyttäjän asetukset', user_settings: 'Käyttäjän asetukset',
@ -160,7 +162,9 @@ const fi = {
notifications: { notifications: {
notifications: 'Ilmoitukset', notifications: 'Ilmoitukset',
read: 'Lue!', read: 'Lue!',
followed_you: 'seuraa sinua' followed_you: 'seuraa sinua',
favorited_you: 'tykkäsi viestistäsi',
repeated_you: 'toisti viestisi'
}, },
login: { login: {
login: 'Kirjaudu sisään', login: 'Kirjaudu sisään',
@ -220,7 +224,9 @@ const en = {
error_fetching: 'Error fetching updates', error_fetching: 'Error fetching updates',
up_to_date: 'Up-to-date', up_to_date: 'Up-to-date',
load_older: 'Load older statuses', load_older: 'Load older statuses',
conversation: 'Conversation' conversation: 'Conversation',
collapse: 'Collapse',
repeated: 'repeated'
}, },
settings: { settings: {
user_settings: 'User Settings', user_settings: 'User Settings',
@ -272,7 +278,9 @@ const en = {
notifications: { notifications: {
notifications: 'Notifications', notifications: 'Notifications',
read: 'Read!', read: 'Read!',
followed_you: 'followed you' followed_you: 'followed you',
favorited_you: 'favorited your status',
repeated_you: 'repeated your status'
}, },
login: { login: {
login: 'Log in', login: 'Log in',