Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85806025
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
21 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 89fcfddba1..9d9f7bbe16 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -1,102 +1,88 @@
-import { reduce, find, filter, sortBy } from 'lodash'
+import { reduce, filter, sortBy } from 'lodash'
import { statusType } from '../../modules/statuses.js'
import Status from '../status/status.vue'
const sortAndFilterConversation = (conversation) => {
conversation = filter(conversation, (status) => statusType(status) !== 'retweet')
return sortBy(conversation, 'id')
}
const conversation = {
data () {
return {
- highlight: null,
- preview: {
- x: 0,
- y: 0,
- status: null
- }
+ highlight: null
}
},
props: [
'statusoid',
'collapsable'
],
computed: {
status () { return this.statusoid },
conversation () {
if (!this.status) {
return false
}
const conversationId = this.status.statusnet_conversation_id
const statuses = this.$store.state.statuses.allStatuses
const conversation = filter(statuses, { statusnet_conversation_id: conversationId })
return sortAndFilterConversation(conversation)
},
replies () {
let i = 1
return reduce(this.conversation, (result, {id, in_reply_to_status_id}) => {
const irid = Number(in_reply_to_status_id)
if (irid) {
result[irid] = result[irid] || []
result[irid].push({
name: `#${i}`,
id: id
})
}
i++
return result
}, {})
}
},
components: {
Status
},
created () {
this.fetchConversation()
},
watch: {
'$route': 'fetchConversation'
},
methods: {
fetchConversation () {
if (this.status) {
const conversationId = this.status.statusnet_conversation_id
this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
.then((statuses) => this.$store.dispatch('addNewStatuses', { statuses }))
.then(() => this.setHighlight(this.statusoid.id))
} else {
const id = this.$route.params.id
this.$store.state.api.backendInteractor.fetchStatus({id})
.then((status) => this.$store.dispatch('addNewStatuses', { statuses: [status] }))
.then(() => this.fetchConversation())
}
},
getReplies (id) {
id = Number(id)
return this.replies[id] || []
},
focused (id) {
if (this.statusoid.retweeted_status) {
return (id === this.statusoid.retweeted_status.id)
} else {
return (id === this.statusoid.id)
}
},
setHighlight (id) {
this.highlight = Number(id)
- },
- setPreview (id, x, y) {
- if (id) {
- this.preview.x = x
- this.preview.y = y
- this.preview.status = find(this.conversation, { id: id })
- } else {
- this.preview.status = null
- }
}
}
}
export default conversation
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index 3dea4985cb..201afcd558 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -1,55 +1,17 @@
<template>
<div class="timeline panel panel-default">
<div class="panel-heading base01-background base04 base03-border conversation-heading">
{{ $t('timeline.conversation') }}
<span v-if="collapsable" style="float:right;">
<small><a href="#" @click.prevent="$emit('toggleExpanded')">Collapse</a></small>
</span>
</div>
<div class="panel-body">
<div class="timeline">
- <status v-for="status in conversation" @goto="setHighlight" :key="status.id" @preview="setPreview" :statusoid="status" :expandable='false' :focused="focused(status.id)" :inConversation='true' :highlight="highlight" :replies="getReplies(status.id)"></status>
- </div>
- </div>
- <div class="status-preview base00-background base03-border" :style="{ left: preview.x + 'px', top: preview.y + 'px'}" v-if="preview.status">
- <img class="avatar" :src="preview.status.user.profile_image_url_original">
- <div class="text">
- <h4>
- {{ preview.status.user.name }}
- <small><a>{{ preview.status.user.screen_name}}</a></small>
- </h4>
- <div @click.prevent="linkClicked" class="status-content" v-html="preview.status.statusnet_html"></div>
+ <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>
</div>
</div>
</div>
</template>
<script src="./conversation.js"></script>
-
-<style lang="scss">
- .status-preview {
- position: absolute;
- max-width: 35em;
- padding: 0.5em;
- display: flex;
- border-color: inherit;
- border-style: solid;
- border-width: 1px;
- border-radius: 4px;
- box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
- .avatar {
- width: 32px;
- height: 32px;
- border-radius: 50%;
- }
- .text {
- h4 {
- margin-bottom: 0.4em;
- small {
- font-weight: lighter;
- }
- }
- padding: 0 0.5em 0.5em 0.5em;
- }
- }
-</style>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 4f5093e11d..55976b8bba 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -1,129 +1,154 @@
import Attachment from '../attachment/attachment.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
import DeleteButton from '../delete_button/delete_button.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
-import { filter } from 'lodash'
+import { filter, find } from 'lodash'
const Status = {
props: [
'statusoid',
'expandable',
'inConversation',
'focused',
'highlight',
'compact',
'replies'
],
data: () => ({
replying: false,
expanded: false,
unmuted: false,
- userExpanded: false
+ userExpanded: false,
+ preview: null,
+ showPreview: false
}),
computed: {
muteWords () {
return this.$store.state.config.muteWords
},
hideAttachments () {
return (this.$store.state.config.hideAttachments && !this.inConversation) ||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation)
},
retweet () { return !!this.statusoid.retweeted_status },
retweeter () { return this.statusoid.user.name },
status () {
if (this.retweet) {
return this.statusoid.retweeted_status
} else {
return this.statusoid
}
},
loggedIn () {
return !!this.$store.state.users.currentUser
},
muteWordHits () {
const statusText = this.status.text.toLowerCase()
const hits = filter(this.muteWords, (muteWord) => {
return statusText.includes(muteWord.toLowerCase())
})
return hits
},
muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
isReply () { return !!this.status.in_reply_to_status_id },
borderColor () {
return {
borderBottomColor: this.$store.state.config.colors['base02']
}
},
isFocused () {
// retweet or root of an expanded conversation
if (this.focused) {
return true
} else if (!this.inConversation) {
return false
}
// use conversation highlight only when in conversation
return this.status.id === this.highlight
}
},
components: {
Attachment,
FavoriteButton,
RetweetButton,
DeleteButton,
PostStatusForm,
UserCardContent
},
methods: {
linkClicked ({target}) {
if (target.tagName === 'SPAN') {
target = target.parentNode
}
if (target.tagName === 'A') {
window.open(target.href, '_blank')
}
},
toggleReplying () {
this.replying = !this.replying
},
gotoOriginal (id) {
// only handled by conversation, not status_or_conversation
- this.$emit('goto', id)
+ if (this.expanded) {
+ this.$emit('goto', id)
+ }
},
toggleExpanded () {
this.$emit('toggleExpanded')
},
toggleMute () {
this.unmuted = !this.unmuted
},
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
replyEnter (id, event) {
+ this.showPreview = true
+ const targetId = Number(id)
+ const statuses = this.$store.state.statuses.allStatuses
+
+ if (!this.preview) {
+ // if we have the status somewhere already
+ this.preview = find(statuses, { 'id': targetId })
+ // or if we have to fetch it
+ if (!this.preview) {
+ this.$store.state.api.backendInteractor.fetchStatus({id}).then((status) => {
+ this.preview = status
+ })
+ }
+ } else if (this.preview.id !== targetId) {
+ this.preview = find(statuses, { 'id': targetId })
+ }
+ /*
if (this.$store.state.config.hoverPreview) {
let rect = event.target.getBoundingClientRect()
this.$emit('preview', Number(id), rect.left + 20, rect.top + 20 + window.pageYOffset)
}
+ */
},
replyLeave () {
+ this.showPreview = false
+ /*
this.$emit('preview', 0, 0, 0)
+ */
}
},
watch: {
'highlight': function (id) {
id = Number(id)
if (this.status.id === id) {
let rect = this.$el.getBoundingClientRect()
if (rect.top < 100) {
window.scrollBy(0, rect.top - 200)
} else if (rect.bottom > window.innerHeight - 50) {
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
}
}
}
}
}
export default Status
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 84397bfa63..c9194ab431 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -1,379 +1,430 @@
<template>
<div class="status-el base00-background" v-if="compact">
<div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></div>
<div v-if="loggedIn">
<div class='status-actions'>
<div>
<a href="#" v-on:click.prevent="toggleReplying">
<i class="fa icon-reply" :class="{'icon-reply-active': replying}"></i>
</a>
</div>
<retweet-button :status=status></retweet-button>
<favorite-button :status=status></favorite-button>
</div>
</div>
<post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" v-on:posted="toggleReplying" v-if="replying"/>
</div>
<div class="status-el base00-background base03-border status-fadein" v-else-if="!status.deleted" v-bind:class="[{ 'base01-background': isFocused }, { 'status-conversation': inConversation }]" >
<template v-if="muted">
<div class="media status container muted">
<small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
<small class="muteWords">{{muteWordHits.join(', ')}}</small>
<a href="#" class="unmute" @click.prevent="toggleMute"><i class="fa icon-eye-off"></i></a>
</div>
</template>
<template v-if="!muted">
<div v-if="retweet" class="media container retweet-info">
<div class="media-left">
<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>
</div>
</div>
<div class="media status container">
<div class="media-left">
<a :href="status.user.statusnet_profile_url">
<img @click.prevent="toggleUserExpanded" :class="{retweeted: retweet}" class='avatar' :src="status.user.profile_image_url_original">
<img v-if="retweet" class='avatar-retweeter' :src="statusoid.user.profile_image_url_original"></img>
</a>
</div>
<div class="media-body">
<div class="base05 base05=border usercard" v-if="userExpanded">
<user-card-content :user="status.user" :switcher="false"></user-card-content>
</div>
<div class="user-content">
<div class="media-heading">
<div class="name-and-links">
<h4 class="user-name">{{status.user.name}}</h4>
<div class="links">
<h4>
<small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
<small v-if="status.in_reply_to_screen_name"> >
<router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }">
{{status.in_reply_to_screen_name}}
</router-link>
</small>
- <template v-if="isReply && !expandable">
+ <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="fa icon-eye-off"></i></a>
<a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="fa icon-binoculars"></i></a>
<template v-if="expandable">
<a href="#" @click.prevent="toggleExpanded" class="expand"><i class="fa icon-plus-squared"></i></a>
</template>
</div>
</div>
+ <div class="status-preview base00-background base03-border" v-if="showPreview && preview">
+ <img class="avatar" :src="preview.user.profile_image_url_original">
+ <div class="text">
+ <h4>
+ {{ preview.user.name }}
+ <small><a>{{ preview.user.screen_name}}</a></small>
+ </h4>
+ <div @click.prevent="linkClicked" class="status-content" v-html="preview.statusnet_html"></div>
+ </div>
+ </div>
+ <div class="status-preview status-preview-loading base00-background base03-border" v-else-if="showPreview">
+ <i class="fa 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">
</attachment>
</div>
</div>
<div v-if="loggedIn">
<div class='status-actions'>
<div>
<a href="#" v-on:click.prevent="toggleReplying">
<i class="fa icon-reply" :class="{'icon-reply-active': replying}"></i>
</a>
</div>
<retweet-button :status=status></retweet-button>
<favorite-button :status=status></favorite-button>
<delete-button :status=status></delete-button>
</div>
</div>
</div>
</div>
<div class="status base00-background 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>
</template>
</div>
</template>
<script src="./status.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
status-text-container {
display: block;
-}
+ }
+
+ .status-preview {
+ position: absolute;
+ max-width: 34em;
+ padding: 0.5em;
+ display: flex;
+ border-color: inherit;
+ border-style: solid;
+ border-width: 1px;
+ border-radius: 4px;
+ box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
+ margin-top: 0.5em;
+ margin-left: 1em;
+
+ .avatar {
+ flex-shrink: 0;
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ }
+ .text {
+ h4 {
+ margin-bottom: 0.4em;
+ small {
+ font-weight: lighter;
+ }
+ }
+ padding: 0 0.5em 0.5em 0.5em;
+ }
+ }
+
+ .status-preview-loading {
+ display: block;
+ font-size: 2em;
+ min-width: 8em;
+ text-align: center;
+ }
.status-el {
hyphens: auto;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
border-left-width: 0px;
line-height: 18px;
.timeline & {
border-bottom-width: 1px;
border-bottom-style: solid;
}
.notify {
.avatar {
border-width: 3px;
border-style: solid;
}
}
.media-body {
flex: 1;
padding-left: 0.5em;
}
.user-content {
min-height: 52px;
padding-top: 1px;
}
.media-heading {
display: flex;
min-height: 1.4em;
margin-bottom: 0.3em;
small {
font-weight: lighter;
}
h4 {
margin-right: 0.4em;
}
.name-and-links {
flex: 1 0;
display: flex;
flex-wrap: wrap;
}
.replies {
flex-basis: 100%;
}
}
.source_url {
}
.expand {
margin-right: -0.3em;
}
a {
display: inline-block;
word-break: break-all;
}
.status-content {
margin: 3px 15px 4px 0;
img, video {
max-width: 100%;
max-height: 400px;
object-fit: contain;
}
blockquote {
margin: 0.2em 0 0.2em 2em;
font-style: italic;
}
}
p {
margin: 0;
margin-top: 0.2em;
margin-bottom: 0.5em;
}
.media-left {
margin: 0.2em 0.3em 0 0;
img {
float: right;
border-radius: 5px;
}
}
.retweet-info {
padding: 0.7em 0 0 0.6em;
.media-left {
display: flex;
i {
align-self: center;
text-align: right;
flex: 1;
padding-right: 0.3em;
}
}
}
}
.status-fadein {
animation-duration: 0.5s;
animation-name: fadein;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.greentext {
color: green;
}
.status-conversation {
border-left-style: solid;
}
.status-actions {
padding-top: 0.15em;
width: 100%;
display: flex;
div, favorite-button {
max-width: 6em;
flex: 1;
}
}
.icon-reply:hover {
color: $blue;
}
.icon-reply-active {
color: $blue;
}
.status .avatar {
width: 48px;
height: 48px;
&.retweeted {
width: 40px;
height: 40px;
margin-right: 8px;
margin-bottom: 8px;
}
}
.status img.avatar-retweeter {
width: 24px;
height: 24px;
position: absolute;
margin-left: 24px;
margin-top: 24px;
}
.status.compact .avatar {
width: 32px;
}
.status {
padding: 0.4em 0.7em 0.45em 0.7em;
border-left: 4px rgba(255, 48, 16, 0.65);
border-left-style: inherit;
}
.status-conversation:last-child {
border-bottom: none;
}
.timeline .panel.timeline {
border-radius: 10px;
overflow: hidden;
}
.muted {
padding: 0.1em 0.4em 0.1em 0.8em;
button {
margin-left: auto;
}
.muteWords {
margin-left: 10px;
}
}
a.unmute {
display: block;
margin-left: auto;
}
.usercard {
border-style: solid;
border-width: 1px;
border-radius: 10px;
margin-bottom: 1em;
margin-top: 0.2em;
}
.reply-left {
flex: 0;
min-width: 48px;
}
.reply-body {
flex: 1;
}
@media all and (max-width: 960px) {
.status-el {
.name-and-links {
margin-left: -0.25em;
}
}
.status {
max-width: 100%;
}
.status .avatar {
width: 40px;
height: 40px;
&.retweeted {
width: 34px;
height: 34px;
margin-right: 8px;
margin-bottom: 8px;
}
}
.status img.avatar-retweeter {
width: 22px;
height: 22px;
position: absolute;
margin-left: 18px;
margin-top: 18px;
}
}
</style>
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Oct 12, 12:52 PM (1 d, 14 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1786203
Default Alt Text
(21 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline
Log In to Comment