Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • pleroma/pleroma-fe
  • eal/pleroma-fe
  • peterspark/pleroma-fe
  • hb2k8/pleroma-fe
  • tibike/pleroma-fe
  • obrez/pleroma-fe
  • partial/pleroma-fe
  • href/pleroma-fe
  • hakabahitoyo/pleroma-fe
  • hsgw/pleroma-fe
  • Azurolu/pleroma-fe
  • cobalto/pleroma-fe
  • qwexvf/pleroma-fe
  • boner.engineer/pleroma-fe
  • f0x/pleroma-fe
  • ataalik/pleroma-fe
  • normandy/pleroma-fe
  • Sir_Boops/pleroma-fe
  • morguldir/pleroma-fe
  • csaurus/pleroma-fe
  • kaniini/pleroma-fe
  • bhtooefr/pleroma-fe
  • andarna/pleroma-fe
  • ktsukik/pleroma-fe
  • Steph/pleroma-fe
  • andrewzah/pleroma-fe
  • lanodan/pleroma-fe
  • pea/pleroma-fe
  • fotfd/pleroma-fe
  • pizzaiolo/pleroma-fe
  • Syldexia/pleroma-fe
  • riking/pleroma-fe
  • dr1ft/pleroma-fe
  • animeirl/pleroma-fe
  • elomatreb/pleroma-fe
  • viv/pleroma-fe
  • goofy/pleroma-fe
  • hoodie/pleroma-fe
  • stolas/pleroma-fe
  • peterpan/pleroma-fe
  • Lumitas/pleroma-fe
  • Toromino/pleroma-fe
  • galen/pleroma-fe
  • scarlett/pleroma-fe
  • ButterflyOfFire/pleroma-fe
  • vaartis/pleroma-fe
  • meireikei/pleroma-fe
  • darko/pleroma-fe
  • pony/pleroma-fe
  • succfemboi/pleroma-fe
  • fadelkon/pleroma-fe
  • dgold/pleroma-fe
  • nebula_moe/pleroma-fe
  • vinzv/pleroma-fe
  • slice/pleroma-fe
  • rinpatch/pleroma-fe
  • maxf/pleroma-fe
  • raeno/pleroma-fe
  • oceanvald/pleroma-fe
  • nuklearfiziks/pleroma-fe
  • feld/pleroma-fe
  • minibikini/pleroma-fe
  • link0ff/pleroma-fe
  • qadeer/pleroma-fe
  • FloatingGhost/pleroma-fe
  • cascode/pleroma-fe
  • hikaruaikawa/pleroma-fe
  • kjwon15/pleroma-fe
  • ukrop/pleroma-fe
  • ilja/pleroma-fe
  • shadowfacts/pleroma-fe
  • edijs/pleroma-fe
  • jdorman632/pleroma-fe
  • xruselfmadex/pleroma-fe
  • futureweb/pleroma-fe
  • eugenijm/pleroma-fe
  • tae/pleroma-fe
  • Dave/pleroma-fe
  • jasper/pleroma-fe
  • Lidar/pleroma-fe
  • parallel588/pleroma-fe
  • jaredr/pleroma-fe
  • rondnelly/pleroma-fe
  • Aditoo/pleroma-fe
  • FongWan/pleroma-fe
  • mkljczk/pleroma-fe
  • nik/pleroma-fe
  • brendenbice1222/pleroma-fe
  • Satak/pleroma-fe
  • xse/pleroma-fe
  • moonman/pleroma-fe
  • Artik/pleroma-fe
  • ssuprunenko/pleroma-fe
  • uncletrunks/pleroma-fe
  • absturztaube/pleroma-fe
  • wyatt777/pleroma-fe
  • hauvophuoc/pleroma-fe
  • dashie/pleroma-fe
  • shmibs/pleroma-fe
  • Elepow/pleroma-fe
  • raven/pleroma-fe
  • buoyantair/pleroma-fe
  • Exilat_a_Tolosa/pleroma-fe
  • matrixsasuke/pleroma-fe
  • njoseph/pleroma-fe
  • ELR/pleroma-fe
  • sjw/pleroma-fe
  • davidyin/pleroma-fe
  • pescetarian/pleroma-fe
  • kphrx/pleroma-fe
  • mewmew/pleroma-fe
  • h3poteto/pleroma-fe
  • Alexpono/pleroma-fe
  • seven/pleroma-fe
  • mparvin/pleroma-fe
  • tuxcrafting/pleroma-fe
  • nekojanai/pleroma-fe
  • xenofem/pleroma-fe
  • p/pleroma-fe
  • creme/pleroma-fe
  • jp/pleroma-fe
  • Jeder/pleroma-fe
  • gensogrips/pleroma-fe
  • caskd/pleroma-fe
  • arkSong/pleroma-fe
  • Hikali/pleroma-fe
  • Duponin/pleroma-fe
  • gashapwn/pleroma-fe
  • fence/pleroma-fe
  • Duder-onomy/pleroma-fe
  • translate/pleroma-fe
  • okl/pleroma-fe
  • bird/pleroma-fe
  • NEETzsche/pleroma-fe
  • Ewoke19CMR/pleroma-fe
  • shevek/pleroma-fe
  • cutienautica/pleroma-fe
  • Nakaya/pleroma-fe
  • Snow/pleroma-fe
  • seanking/pleroma-fe
  • kkcake/pleroma-fe
  • Testacc/pleroma-fe
  • flxy/pleroma-fe
  • xerz/pleroma-fe
  • maronu/pleroma-fe
  • matildepark/pleroma-fe
  • Craftplacer/pleroma-fe
147 results
Show changes
Showing
with 1444 additions and 109 deletions
<template> <template>
<div class="status-el"> <div class="status-el base00-background" v-if="compact">
<div v-if="retweet" class="media container retweet-info"> <div @click.prevent="linkClicked" class="status-content" v-html="status.statusnet_html"></div>
<div class="media-left"> <div v-if="loggedIn">
<i class='fa icon-retweet'></i> <div class='status-actions'>
</div> <div>
<div class="media-body"> <a href="#" v-on:click.prevent="toggleReplying">
Retweeted by {{retweeter}} <i class="base09 icon-reply" :class="{'icon-reply-active': replying}"></i>
</a>
</div>
<retweet-button :status=status></retweet-button>
<favorite-button :status=status></favorite-button>
</div> </div>
</div> </div>
<div class="media status container" ng-class="{compact: compact, notify: notify}"> <post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" v-on:posted="toggleReplying" v-if="replying"/>
<div class="media-left"> </div>
<a :href="status.user.statusnet_profile_url"> <div class="status-el base00-background base03-border status-fadein" v-else-if="!status.deleted" v-bind:class="[{ 'base01-background': isFocused }, { 'status-conversation': inConversation }]" >
<img class='avatar' :src="status.user.profile_image_url_original"> <template v-if="muted">
</a> <div class="media status container muted">
<small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
<small class="muteWords">{{muteWordHits.join(', ')}}</small>
<a href="#" class="unmute" @click.prevent="toggleMute"><i class="base09 icon-eye-off"></i></a>
</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>
<div class="media-body"> <div class="media status container">
<h4 class="media-heading"> <div class="media-left">
<strong>{{status.user.name}}</strong> <a :href="status.user.statusnet_profile_url">
<small><a :href="status.user.statusnet_profile_url">{{status.user.screen_name}}</a></small> <img @click.prevent="toggleUserExpanded" :class="{retweeted: retweet}" class='avatar' :src="status.user.profile_image_url_original">
<small v-if="status.in_reply_to_screen_name"> &gt; <a :href="status.in_reply_to_profileurl">{{status.in_reply_to_screen_name}}</a></small> <img v-if="retweet" class='avatar-retweeter' :src="statusoid.user.profile_image_url_original"></img>
- </a>
<small>{{status.created_at_parsed}}</small>
</h4>
<div class="status-content" v-html="status.statusnet_html"></div>
<div v-if='status.attachments' class='attachments'>
<attachment :status-id="status.id" :nsfw="status.nsfw" :attachment="attachment" v-for="attachment in status.attachments">
</attachment>
</div> </div>
<div class="media-body">
<div class="base03-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"> &gt;
<router-link :to="{ name: 'user-profile', params: { id: status.in_reply_to_user_id } }">
{{status.in_reply_to_screen_name}}
</router-link>
</small>
<template v-if="isReply">
<small>
<a href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)"><i class="icon-reply" @mouseenter="replyEnter(status.in_reply_to_status_id, $event)" @mouseout="replyLeave()"></i></a>
</small>
</template>
-
<small>
<router-link :to="{ name: 'conversation', params: { id: status.id } }">
<timeago :since="status.created_at" :auto-update="60"></timeago>
</router-link>
</small>
</h4>
</div>
<h4 class="replies" v-if="inConversation">
<small v-if="replies.length">Replies:</small>
<small v-for="reply in replies">
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}}&nbsp;</a>
</small>
</h4>
</div>
<div class="heading-icons">
<a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="base09 icon-eye-off"></i></a>
<a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="base09 icon-binoculars"></i></a>
<template v-if="expandable">
<a href="#" @click.prevent="toggleExpanded" class="expand"><i class="base09 icon-plus-squared"></i></a>
</template>
</div>
</div>
<div v-if="loggedIn"> <div class="status-preview base00-background base03-border" v-if="showPreview && preview">
<div class='status-actions'> <img class="avatar" :src="preview.user.profile_image_url_original">
<div> <div class="text">
<a href="#" v-on:click.prevent="toggleReplying"> <h4>
<i class='fa icon-reply'></i> {{ preview.user.name }}
</a> <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="base09 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>
<retweet-button :status=status></retweet-button>
<favorite-button :status=status></favorite-button>
</div> </div>
<post-status-form v-if="replying" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" v-on:posted="toggleReplying"></post-status-form> <div v-if="loggedIn">
<div class='status-actions'>
<div>
<a href="#" v-on:click.prevent="toggleReplying">
<i class="base09 icon-reply" :class="{'icon-reply-active': replying}"></i>
</a>
</div>
<retweet-button :status=status></retweet-button>
<favorite-button :status=status></favorite-button>
<delete-button :status=status></delete-button>
</div>
</div>
</div> </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> </div>
</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-el {
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; hyphens: auto;
overflow-wrap: break-word; overflow-wrap: break-word;
word-wrap: break-word; word-wrap: break-word;
word-break: 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 { a {
display: inline-block; display: inline-block;
...@@ -64,16 +240,187 @@ ...@@ -64,16 +240,187 @@
} }
.status-content { .status-content {
margin-top: 3px; margin: 3px 15px 4px 0;
margin-bottom: 3px; max-height: 400px;
overflow-y: auto;
overflow-x: hidden;
img, video {
max-width: 100%;
max-height: 400px;
object-fit: contain;
}
blockquote {
margin: 0.2em 0 0.2em 2em;
font-style: italic;
}
} }
}
.status-actions { p {
padding-top: 5px; margin: 0;
} margin-top: 0.2em;
margin-bottom: 0.5em;
}
.icon-reply:hover { .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; color: $blue;
} }
.icon-reply.icon-reply-active {
color: $blue;
}
.status .avatar {
width: 48px;
height: 48px;
&.retweeted {
width: 40px;
height: 40px;
margin-right: 8px;
margin-bottom: 8px;
}
}
.status img.avatar-retweeter {
width: 24px;
height: 24px;
position: absolute;
margin-left: 24px;
margin-top: 24px;
}
.status.compact .avatar {
width: 32px;
}
.status {
padding: 0.4em 0.7em 0.45em 0.7em;
border-left: 4px rgba(255, 48, 16, 0.65);
border-left-style: inherit;
}
.status-conversation:last-child {
border-bottom: none;
}
.timeline .panel.timeline {
border-radius: 10px;
overflow: hidden;
}
.muted {
padding: 0.1em 0.4em 0.1em 0.8em;
button {
margin-left: auto;
}
.muteWords {
margin-left: 10px;
}
}
a.unmute {
display: block;
margin-left: auto;
}
.reply-left {
flex: 0;
min-width: 48px;
}
.reply-body {
flex: 1;
}
@media all and (max-width: 960px) {
.status-el {
.name-and-links {
margin-left: -0.25em;
}
}
.status {
max-width: 100%;
}
.status .avatar {
width: 40px;
height: 40px;
&.retweeted {
width: 34px;
height: 34px;
margin-right: 8px;
margin-bottom: 8px;
}
}
.status img.avatar-retweeter {
width: 22px;
height: 22px;
position: absolute;
margin-left: 18px;
margin-top: 18px;
}
}
</style> </style>
import Status from '../status/status.vue'
import Conversation from '../conversation/conversation.vue'
const statusOrConversation = {
props: ['statusoid'],
data () {
return {
expanded: false
}
},
components: {
Status,
Conversation
},
methods: {
toggleExpanded () {
this.expanded = !this.expanded
}
}
}
export default statusOrConversation
<template>
<div>
<conversation v-if="expanded" @toggleExpanded="toggleExpanded" :collapsable="true" :statusoid="statusoid"></conversation>
<status v-if="!expanded" @toggleExpanded="toggleExpanded" :expandable="true" :inConversation="false" :focused="false" :statusoid="statusoid"></status>
</div>
</template>
<script src="./status_or_conversation.js"></script>
<style lang="scss">
.spacer {
height: 1em
}
</style>
import { rgbstr2hex } from '../../services/color_convert/color_convert.js'
export default {
data () {
return {
availableStyles: [],
selected: this.$store.state.config.theme,
bgColorLocal: '',
fgColorLocal: '',
textColorLocal: '',
linkColorLocal: ''
}
},
created () {
const self = this
window.fetch('/static/styles.json')
.then((data) => data.json())
.then((themes) => {
self.availableStyles = themes
})
},
mounted () {
this.bgColorLocal = rgbstr2hex(this.$store.state.config.colors['base00'])
this.fgColorLocal = rgbstr2hex(this.$store.state.config.colors['base02'])
this.textColorLocal = rgbstr2hex(this.$store.state.config.colors['base05'])
this.linkColorLocal = rgbstr2hex(this.$store.state.config.colors['base08'])
},
methods: {
setCustomTheme () {
if (!this.bgColorLocal && !this.fgColorLocal && !this.linkColorLocal) {
// reset to picked themes
}
const rgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null
}
const bgRgb = rgb(this.bgColorLocal)
const fgRgb = rgb(this.fgColorLocal)
const textRgb = rgb(this.textColorLocal)
const linkRgb = rgb(this.linkColorLocal)
if (bgRgb && fgRgb && linkRgb) {
this.$store.dispatch('setOption', {
name: 'customTheme',
value: {
fg: fgRgb,
bg: bgRgb,
text: textRgb,
link: linkRgb
}})
}
}
},
watch: {
selected () {
this.bgColorLocal = this.selected[1]
this.fgColorLocal = this.selected[2]
this.textColorLocal = this.selected[3]
this.linkColorLocal = this.selected[4]
}
}
}
<template>
<div>
<p>{{$t('settings.presets')}}</p>
<select v-model="selected" class="style-switcher">
<option v-for="style in availableStyles" :value="style">{{style[0]}}</option>
</select>
<p>{{$t('settings.theme_help')}}</p>
<div class="color-container">
<div class="color-item">
<label for="bgcolor" class="base04">{{$t('settings.background')}}</label>
<input id="bgcolor" class="theme-color-in" type="text" v-model="bgColorLocal">
</div>
<div class="color-item">
<label for="fgcolor" class="base04">{{$t('settings.foreground')}}</label>
<input id="fgcolor" class="theme-color-in" type="text" v-model="fgColorLocal">
</div>
<div class="color-item">
<label for="textcolor" class="base04">{{$t('settings.text')}}</label>
<input id="textcolor" class="theme-color-in" type="text" v-model="textColorLocal">
</div>
<div class="color-item">
<label for="linkcolor" class="base04">{{$t('settings.links')}}</label>
<input id="linkcolor" class="theme-color-in" type="text" v-model="linkColorLocal">
</div>
</div>
<div>
<div class="panel">
<div class="panel-heading" :style="{ 'background-color': fgColorLocal, 'color': textColorLocal }">Preview</div>
<div class="panel-body theme-preview-content" :style="{ 'background-color': bgColorLocal, 'color': textColorLocal }">
<h4>Content</h4>
<br>
A bunch of more content and
<a :style="{ 'color': linkColorLocal }">a nice lil' link</a>
<br>
<button class="btn" :style="{ 'background-color': fgColorLocal, 'color': textColorLocal }">Button</button>
</div>
</div>
</div>
<button class="btn base02-background base04" @click="setCustomTheme">{{$t('general.apply')}}</button>
</div>
</template>
<script src="./style_switcher.js"></script>
<style lang="scss">
.style-switcher {
margin-right: 1em;
}
.color-container {
display: flex;
flex-wrap: wrap;
}
.color-item {
max-width: 9em;
display:flex;
flex-wrap:wrap;
}
.theme-color-in {
max-width: 8em;
border-radius: 2px;
border: 0;
padding: 5px;
margin: 5px 0 5px 0;
}
.theme-preview-content {
padding: 20px;
}
</style>
import Timeline from '../timeline/timeline.vue'
const TagTimeline = {
created () {
this.$store.commit('clearTimeline', { timeline: 'tag' })
this.$store.dispatch('startFetching', { 'tag': this.tag })
},
components: {
Timeline
},
computed: {
tag () { return this.$route.params.tag },
timeline () { return this.$store.state.statuses.timelines.tag }
},
watch: {
tag () {
this.$store.commit('clearTimeline', { timeline: 'tag' })
this.$store.dispatch('startFetching', { 'tag': this.tag })
}
},
destroyed () {
this.$store.dispatch('stopFetching', 'tag')
}
}
export default TagTimeline
<template>
<Timeline :title="tag" :timeline="timeline" :timeline-name="'tag'" :tag="tag" />
</template>
<script src='./tag_timeline.js'></script>
\ No newline at end of file
import Status from '../status/status.vue' import Status from '../status/status.vue'
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js' import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue'
import UserCard from '../user_card/user_card.vue'
const Timeline = { const Timeline = {
props: [ props: [
'timeline', 'timeline',
'timelineName' 'timelineName',
'title',
'userId',
'tag'
], ],
data () {
return {
paused: false
}
},
computed: {
timelineError () { return this.$store.state.statuses.error },
followers () {
return this.timeline.followers
},
friends () {
return this.timeline.friends
},
viewing () {
return this.timeline.viewing
},
newStatusCount () {
return this.timeline.newStatusCount
},
newStatusCountStr () {
if (this.timeline.flushMarker !== 0) {
return ''
} else {
return ` (${this.newStatusCount})`
}
}
},
components: { components: {
Status Status,
StatusOrConversation,
UserCard
}, },
created () { created () {
const store = this.$store const store = this.$store
const credentials = store.state.users.currentUser.credentials const credentials = store.state.users.currentUser.credentials
const showImmediately = this.timeline.visibleStatuses.length === 0
window.addEventListener('scroll', this.scrollLoad)
timelineFetcher.fetchAndUpdate({ timelineFetcher.fetchAndUpdate({
store, store,
credentials, credentials,
timeline: this.timelineName, timeline: this.timelineName,
showImmediately: true showImmediately,
userId: this.userId,
tag: this.tag
}) })
// don't fetch followers for public, friend, twkn
if (this.timelineName === 'user') {
this.fetchFriends()
this.fetchFollowers()
}
},
destroyed () {
window.removeEventListener('scroll', this.scrollLoad)
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
}, },
methods: { methods: {
showNewStatuses () { showNewStatuses () {
this.$store.commit('showNewStatuses', { timeline: this.timelineName }) if (this.timeline.flushMarker !== 0) {
this.$store.commit('clearTimeline', { timeline: this.timelineName })
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
this.fetchOlderStatuses()
} else {
this.$store.commit('showNewStatuses', { timeline: this.timelineName })
this.paused = false
}
}, },
fetchOlderStatuses () { fetchOlderStatuses () {
const store = this.$store const store = this.$store
...@@ -33,8 +89,44 @@ const Timeline = { ...@@ -33,8 +89,44 @@ const Timeline = {
credentials, credentials,
timeline: this.timelineName, timeline: this.timelineName,
older: true, older: true,
showImmediately: true showImmediately: true,
userId: this.userId,
tag: this.tag
}).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false })) }).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false }))
},
fetchFollowers () {
const id = this.userId
this.$store.state.api.backendInteractor.fetchFollowers({ id })
.then((followers) => this.$store.dispatch('addFollowers', { followers }))
},
fetchFriends () {
const id = this.userId
this.$store.state.api.backendInteractor.fetchFriends({ id })
.then((friends) => this.$store.dispatch('addFriends', { friends }))
},
scrollLoad (e) {
let height = Math.max(document.body.offsetHeight, document.body.scrollHeight)
if (this.timeline.loading === false &&
this.$store.state.config.autoLoad &&
this.$el.offsetHeight > 0 &&
(window.innerHeight + window.pageYOffset) >= (height - 750)) {
this.fetchOlderStatuses()
}
}
},
watch: {
newStatusCount (count) {
if (!this.$store.state.config.streaming) {
return
}
if (count > 0) {
// only 'stream' them when you're scrolled to the top
if (window.pageYOffset < 15 && !this.paused) {
this.showNewStatuses()
} else {
this.paused = true
}
}
} }
} }
} }
......
<template> <template>
<div class="timeline"> <div class="timeline panel panel-default" v-if="viewing == 'statuses'">
<a href="#" v-on:click.prevent='showNewStatuses()' v-if="timeline.newStatusCount > 0"> <div class="panel-heading timeline-heading base02-background base04">
<div class="new-status-notification"> <div class="title">
<p class="text-center" > {{title}}
{{timeline.newStatusCount}} new statuses </div>
</p> <button @click.prevent="showNewStatuses" class="base05 base02-background loadmore-button" v-if="timeline.newStatusCount > 0 && !timelineError">
</div> {{$t('timeline.show_new')}}{{newStatusCountStr}}
</a> </button>
<status v-for="status in timeline.visibleStatuses" :key="status.id" v-bind:statusoid="status"></status> <div @click.prevent class="base06 error loadmore-text" v-if="timelineError">
<a href="#" v-on:click.prevent='fetchOlderStatuses()' v-if="!timeline.loading"> {{$t('timeline.error_fetching')}}
<div class="new-status-notification"> </div>
<p class="text-center" > <div @click.prevent class="base04 base02-background loadmore-text" v-if="!timeline.newStatusCount > 0 && !timelineError">
Load older statuses. {{$t('timeline.up_to_date')}}
</p> </div>
</div> </div>
</a> <div class="panel-body base01-background">
<div class="timeline">
<status-or-conversation v-for="status in timeline.visibleStatuses" :key="status.id" v-bind:statusoid="status"></status-or-conversation>
<a href="#" v-on:click.prevent='fetchOlderStatuses()' v-if="!timeline.loading">
<div class="base02-background base03-border new-status-notification text-center">{{$t('timeline.load_older')}}</div>
</a>
<div class="base02-background base03-border new-status-notification text-center" v-else>...</div>
</div>
</div>
</div>
<div class="timeline panel panel-default" v-else-if="viewing == 'followers'">
<div class="panel-heading timeline-heading base02-background base04">
<div class="title">
{{$t('user_card.followers')}}
</div>
</div>
<div class="panel-body base01-background">
<div class="timeline">
<user-card v-for="follower in followers" :user="follower" :showFollows="false"></user-card>
</div>
</div>
</div>
<div class="timeline panel panel-default" v-else-if="viewing == 'friends'">
<div class="panel-heading timeline-heading base02-background base04">
<div class="title">
{{$t('user_card.followees')}}
</div>
</div>
<div class="panel-body base01-background">
<div class="timeline">
<user-card v-for="friend in friends" :user="friend" :showFollows="true"></user-card>
</div>
</div>
</div> </div>
</template> </template>
<script src="./timeline.js"></script> <script src="./timeline.js"></script>
<style> <style lang="scss">
.timeline.panel {
flex: 2; .timeline {
flex-basis: 500px; .timeline-heading {
} position: relative;
display: flex;
}
.title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 70%;
}
.loadmore-button {
position: absolute;
right: 0.6em;
font-size: 14px;
min-width: 6em;
height: 1.8em;
line-height: 100%;
}
.loadmore-text {
position: absolute;
right: 0.6em;
font-size: 14px;
min-width: 6em;
border-radius: 5px;
font-family: sans-serif;
text-align: center;
padding: 0 0.5em 0 0.5em;
opacity: 0.8;
}
.error {
background-color: rgba(255, 48, 16, 0.65);
}
}
.new-status-notification {
position:relative;
margin-top: -1px;
font-size: 1.1em;
border-width: 1px 0 0 0;
border-style: solid;
border-radius: 0 0 10px 10px;
padding: 10px;
z-index: 1;
}
</style> </style>
import UserCardContent from '../user_card_content/user_card_content.vue'
const UserCard = {
props: [
'user',
'showFollows'
],
data () {
return {
userExpanded: false
}
},
components: {
UserCardContent
},
methods: {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
}
}
}
export default UserCard
<template>
<div class="card base00-background">
<a href="#">
<img @click.prevent="toggleUserExpanded" class="avatar" :src="user.profile_image_url">
</a>
<div class="usercard" v-if="userExpanded">
<user-card-content :user="user" :switcher="false"></user-card-content>
</div>
<div class="name-and-screen-name" v-else>
<div class="user-name">
{{ user.name }}
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
{{ $t('user_card.follows_you') }}
</span>
</div>
<a :href="user.statusnet_profile_url" target="blank"><div class="user-screen-name">@{{ user.screen_name }}</div></a>
</div>
</div>
</template>
<script src="./user_card.js"></script>
<style lang="scss">
.name-and-screen-name {
margin-left: 0.7em;
margin-top:0.0em;
margin-right: 2em;
text-align: left;
width: 100%;
}
.follows-you {
margin-left: 2em;
float: right;
}
.follows {
}
.card {
display: flex;
flex: 1 0;
padding-top: 0.6em;
padding-right: 1em;
padding-bottom: 0.6em;
padding-left: 1em;
border-bottom: 1px solid;
margin: 0;
border-bottom-color: inherit;
.avatar {
margin-top: 0.2em;
width:32px;
height: 32px;
border-radius: 50%;
}
}
.usercard {
width: fill-available;
margin: 0.2em 0 0.7em 0;
border-radius: 10px;
border-style: solid;
border-color: inherit;
border-width: 1px;
overflow: hidden;
p {
margin-bottom: 0;
}
}
</style>
import { hex2rgb } from '../../services/color_convert/color_convert.js'
export default {
props: [ 'user', 'switcher' ],
computed: {
headingStyle () {
const color = this.$store.state.config.colors['base00']
if (color) {
const rgb = hex2rgb(color)
console.log(rgb)
return {
backgroundColor: `rgb(${Math.floor(rgb[0] * 0.53)}, ${Math.floor(rgb[1] * 0.56)}, ${Math.floor(rgb[2] * 0.59)})`,
backgroundImage: `url(${this.user.cover_photo})`
}
}
},
bodyStyle () {
return {
background: `linear-gradient(to bottom, rgba(0, 0, 0, 0), ${this.$store.state.config.colors['base00']} 80%)`
}
},
isOtherUser () {
return this.user.id !== this.$store.state.users.currentUser.id
},
loggedIn () {
return this.$store.state.users.currentUser
},
dailyAvg () {
const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000))
return Math.round(this.user.statuses_count / days)
}
},
methods: {
followUser () {
const store = this.$store
store.state.api.backendInteractor.followUser(this.user.id)
.then((followedUser) => store.commit('addNewUsers', [followedUser]))
},
unfollowUser () {
const store = this.$store
store.state.api.backendInteractor.unfollowUser(this.user.id)
.then((unfollowedUser) => store.commit('addNewUsers', [unfollowedUser]))
},
blockUser () {
const store = this.$store
store.state.api.backendInteractor.blockUser(this.user.id)
.then((blockedUser) => store.commit('addNewUsers', [blockedUser]))
},
unblockUser () {
const store = this.$store
store.state.api.backendInteractor.unblockUser(this.user.id)
.then((unblockedUser) => store.commit('addNewUsers', [unblockedUser]))
},
toggleMute () {
const store = this.$store
store.commit('setMuted', {user: this.user, muted: !this.user.muted})
store.state.api.backendInteractor.setUserMute(this.user)
},
setProfileView (v) {
const store = this.$store
store.commit('setProfileView', { v })
}
}
}
<template>
<div id="heading" class="profile-panel-background" :style="headingStyle">
<div class="panel-heading text-center">
<div class='user-info'>
<router-link to='/user-settings' style="float: right; margin-top:16px;" v-if="!isOtherUser">
<i class="icon-cog usersettings"></i>
</router-link>
<div class='container'>
<router-link :to="{ name: 'user-profile', params: { id: user.id } }">
<img :src="user.profile_image_url_original">
</router-link>
<span class="glyphicon glyphicon-user"></span>
<div class="name-and-screen-name">
<div class='user-name'>{{user.name}}</div>
<router-link :to="{ name: 'user-profile', params: { id: user.id } }">
<div class='user-screen-name'>@{{user.screen_name}}</div>
</router-link>
</div>
</div>
<div v-if="isOtherUser" class="user-interactions">
<div v-if="user.follows_you && loggedIn" class="following base06">
{{ $t('user_card.follows_you') }}
</div>
<div class="follow" v-if="loggedIn">
<span v-if="user.following">
<!--Following them!-->
<button @click="unfollowUser" class="base04 base00-background pressed">
{{ $t('user_card.following') }}
</button>
</span>
<span v-if="!user.following">
<button @click="followUser" class="base05 base02-background">
{{ $t('user_card.follow') }}
</button>
</span>
</div>
<div class='mute' v-if='isOtherUser'>
<span v-if='user.muted'>
<button @click="toggleMute" class="base04 base00-background pressed">
{{ $t('user_card.muted') }}
</button>
</span>
<span v-if='!user.muted'>
<button @click="toggleMute" class="base05 base02-background">
{{ $t('user_card.mute') }}
</button>
</span>
</div>
<div class='block' v-if='isOtherUser && loggedIn'>
<span v-if='user.statusnet_blocking'>
<button @click="unblockUser" class="base04 base00-background pressed">
{{ $t('user_card.blocked') }}
</button>
</span>
<span v-if='!user.statusnet_blocking'>
<button @click="blockUser" class="base05 base02-background">
{{ $t('user_card.block') }}
</button>
</span>
</div>
</div>
</div>
</div>
<div class="panel-body profile-panel-body" :style="bodyStyle">
<div class="user-counts">
<div class="user-count">
<a href="#" v-on:click.prevent="setProfileView('statuses')" v-if="switcher"><h5 class="base05">{{ $t('user_card.statuses') }}</h5></a>
<h5 v-else>{{ $t('user_card.statuses') }}</h5>
<span class="base05">{{user.statuses_count}} <br><span class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span></span>
</div>
<div class="user-count">
<a href="#" v-on:click.prevent="setProfileView('friends')" v-if="switcher"><h5 class="base05">{{ $t('user_card.followees') }}</h5></a>
<h5 v-else>{{ $t('user_card.followees') }}</h5>
<span class="base05">{{user.friends_count}}</span>
</div>
<div class="user-count">
<a href="#" v-on:click.prevent="setProfileView('followers')" v-if="switcher"><h5 class="base05">{{ $t('user_card.followers') }}</h5></a>
<h5 v-else>{{ $t('user_card.followers') }}</h5>
<span class="base05">{{user.followers_count}}</span>
</div>
</div>
<p>{{user.description}}</p>
</div>
</div>
</template>
<script src="./user_card_content.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.profile-panel-background {
background-size: cover;
border-radius: 10px;
.panel-heading {
padding: 0.6em 0em;
text-align: center;
}
}
.profile-panel-body {
top: -0em;
padding-top: 4em;
word-wrap: break-word;
}
.user-info {
color: white;
padding: 0 16px 16px 16px;
margin-bottom: -4em;
.usersettings {
color: white;
opacity: 0.8;
}
.container{
padding: 16px 10px 4px 10px;
display: flex;
flex-wrap: wrap;
flex-direction: column;
align-content: flex-start;
justify-content: center;
max-height: 56px;
overflow: hidden;
}
img {
border-radius: 5px;
flex: 1 0 100%;
width: 56px;
height: 56px;
box-shadow: 0px 1px 8px rgba(0,0,0,0.75);
object-fit: cover;
}
text-shadow: 0px 1px 1.5px rgba(0, 0, 0, 1.0);
.name-and-screen-name {
display: block;
margin-left: 0.6em;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-name{
color: white;
}
.user-screen-name {
color: white;
font-weight: lighter;
font-size: 15px;
padding-right: 0.1em;
flex: 0 0 auto;
}
.user-interactions {
display: flex;
flex-flow: row wrap;
justify-content: space-between;
div {
flex: 1;
}
margin-top: 0.7em;
margin-bottom: -1.0em;
.following {
color: white;
font-size: 14px;
flex: 0 0 100%;
margin: -0.7em 0.0em 0.3em 0.0em;
padding-left: 16px;
text-align: left;
}
.mute {
max-width: 220px;
min-height: 28px;
}
.follow {
max-width: 220px;
min-height: 28px;
}
button {
width: 92%;
height: 100%;
}
.pressed {
border-bottom-color: rgba(255, 255, 255, 0.2);
border-top-color: rgba(0, 0, 0, 0.2);
}
}
}
.user-counts {
display: flex;
line-height:16px;
padding: 1em 1.5em 0em 1em;
text-align: center;
}
.user-count {
flex: 1;
h5 {
font-size:1em;
font-weight: bolder;
margin: 0 0 0.25em;
}
a {
text-decoration: none;
}
}
.dailyAvg {
font-size: 0.8em;
opacity: 0.5;
}
</style>
const UserFinder = {
data: () => ({
username: undefined,
hidden: true,
error: false,
loading: false
}),
methods: {
findUser (username) {
username = username[0] === '@' ? username.slice(1) : username
this.loading = true
this.$store.state.api.backendInteractor.externalProfile(username)
.then((user) => {
this.loading = false
this.hidden = true
if (!user.error) {
this.$store.commit('addNewUsers', [user])
this.$router.push({name: 'user-profile', params: {id: user.id}})
} else {
this.error = true
}
})
},
toggleHidden () {
this.hidden = !this.hidden
},
dismissError () {
this.error = false
}
}
}
export default UserFinder
<template>
<span class="user-finder-container">
<span class="finder-error base05" v-if="error">
<i class="icon-cancel user-finder-icon" @click="dismissError"/>
{{$t('finder.error_fetching_user')}}
</span>
<i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
<a href="#" v-if="hidden"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden"/></a>
<span v-else>
<input class="user-finder-input base03-border" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
<i class="icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
</span>
</span>
</template>
<script src="./user_finder.js"></script>
<style lang="scss">
.user-finder-container {
height: 21px;
max-width: 100%;
}
.user-finder-icon {
}
.user-finder-input {
border-width: 1px;
border-style: solid;
border-color: inherit;
border-radius: 5px;
max-width: 80%;
padding: 0.1em 0.2em 0.2em 0.2em;
}
.finder-error {
background-color: rgba(255, 48, 16, 0.65);
margin: 0.35em;
border-radius: 5px;
padding: 0.25em;
}
</style>
import LoginForm from '../login_form/login_form.vue' import LoginForm from '../login_form/login_form.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue' import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
const UserPanel = { const UserPanel = {
computed: { computed: {
user () { return this.$store.state.users.currentUser }, user () { return this.$store.state.users.currentUser }
style () {
return {
color: `#${this.user.profile_link_color}`,
'background-image': `url(${this.user.cover_photo})`
}
}
}, },
components: { components: {
LoginForm, LoginForm,
PostStatusForm PostStatusForm,
UserCardContent
} }
} }
......
<template> <template>
<div class="user-panel"> <div class="user-panel">
<div v-if='user' class="panel panel-default"> <div v-if='user' class="panel panel-default" style="overflow: visible;">
<!-- Default panel contents --> <user-card-content :user="user" :switcher="false"></user-card-content>
<div class="panel-heading text-center" v-bind:style="style"> <div class="panel-footer base00-background">
<div class='user-info'>
<img :src="user.profile_image_url">
<span class="glyphicon glyphicon-user"></span>
<div class='user-name'>{{user.name}}</div>
<div class='user-screen-name'>@{{user.screen_name}}</div>
</div>
</div>
<div class="panel-body">
<div class="user-counts">
<div class="user-count">
<h5>Statuses</h5>
<span>{{user.statuses_count}}</span>
</div>
<div class="user-count">
<h5>Following</h5>
<span>{{user.friends_count}}</span>
</div>
<div class="user-count">
<h5>Followers</h5>
<span>{{user.followers_count}}</span>
</div>
</div>
<p>{{user.description}}</p>
</div>
<div class="panel-footer">
<post-status-form v-if='user'></post-status-form> <post-status-form v-if='user'></post-status-form>
</div> </div>
</div> </div>
......
import UserCardContent from '../user_card_content/user_card_content.vue'
import Timeline from '../timeline/timeline.vue'
const UserProfile = {
created () {
this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.dispatch('startFetching', ['user', this.userId])
if (!this.$store.state.users.usersObject[this.userId]) {
this.$store.dispatch('fetchUser', this.userId)
}
},
destroyed () {
this.$store.dispatch('stopFetching', 'user')
},
computed: {
timeline () { return this.$store.state.statuses.timelines.user },
userId () {
return this.$route.params.id
},
user () {
if (this.timeline.statuses[0]) {
return this.timeline.statuses[0].user
} else {
return this.$store.state.users.usersObject[this.userId] || false
}
}
},
watch: {
userId () {
this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.dispatch('startFetching', ['user', this.userId])
}
},
components: {
UserCardContent,
Timeline
}
}
export default UserProfile
<template>
<div>
<div v-if="user" class="user-profile panel panel-default base00-background">
<user-card-content :user="user" :switcher="true"></user-card-content>
</div>
<Timeline :title="'User Timeline'" v-bind:timeline="timeline" v-bind:timeline-name="'user'" :user-id="userId"/>
</div>
</template>
<script src="./user_profile.js"></script>
<style lang="scss">
.user-profile {
flex: 2;
flex-basis: 500px;
padding-bottom: 10px;
border-radius: 10px;
}
</style>
This diff is collapsed.