Merge remote-tracking branch 'upstream/develop' into shigusegubu
* upstream/develop: (108 commits) Update user settings text: staff badges are only visible in user profile Fix naming Show back followees/followers counts Always show my own followees/followers Hide followers and followees tabs when they are disabled #137 - clean up emoji list #315 - revert changes #315 - separate export and fetch follows logic #137 - add red heart emoji Grammar Fix repeater html not showing properly #301 - add an option for default formatting #315 - clean up the request remove unnecessary div #315 - export all follows as CSV Make role badge visible only on user profile page Add theming support for admin/moderator badges Refactor visibleRole for better readability Allow to configure visibility for admin and moderator badges Add admin and moderator indicators to the user card ...
This commit is contained in:
commit
3f08301d6b
99 changed files with 3404 additions and 430 deletions
|
|
@ -7,6 +7,9 @@ const About = {
|
|||
InstanceSpecificPanel,
|
||||
FeaturesPanel,
|
||||
TermsOfServicePanel
|
||||
},
|
||||
computed: {
|
||||
showFeaturesPanel () { return this.$store.state.config.showFeaturesPanel }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="sidebar">
|
||||
<instance-specific-panel></instance-specific-panel>
|
||||
<features-panel></features-panel>
|
||||
<features-panel v-if="showFeaturesPanel"></features-panel>
|
||||
<terms-of-service-panel></terms-of-service-panel>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import StillImage from '../still-image/still-image.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
import nsfwImage from '../../assets/nsfw.png'
|
||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
|
||||
|
|
@ -7,23 +8,29 @@ const Attachment = {
|
|||
'attachment',
|
||||
'nsfw',
|
||||
'statusId',
|
||||
'size'
|
||||
'size',
|
||||
'allowPlay',
|
||||
'setMedia'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
||||
hideNsfwLocal: this.$store.state.config.hideNsfw,
|
||||
preloadImage: this.$store.state.config.preloadImage,
|
||||
loopVideo: this.$store.state.config.loopVideo,
|
||||
showHidden: false,
|
||||
loading: false,
|
||||
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img')
|
||||
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
|
||||
modalOpen: false,
|
||||
showHidden: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
StillImage
|
||||
StillImage,
|
||||
VideoAttachment
|
||||
},
|
||||
computed: {
|
||||
usePlaceHolder () {
|
||||
return this.size === 'hide' || this.type === 'unknown'
|
||||
},
|
||||
referrerpolicy () {
|
||||
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
|
||||
},
|
||||
|
|
@ -40,7 +47,7 @@ const Attachment = {
|
|||
return this.size === 'small'
|
||||
},
|
||||
fullwidth () {
|
||||
return fileTypeService.fileType(this.attachment.mimetype) === 'html'
|
||||
return this.type === 'html' || this.type === 'audio'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -49,7 +56,24 @@ const Attachment = {
|
|||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
toggleHidden () {
|
||||
openModal (event) {
|
||||
const modalTypes = this.$store.state.config.playVideosInModal
|
||||
? ['image', 'video']
|
||||
: ['image']
|
||||
if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) ||
|
||||
this.usePlaceHolder
|
||||
) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
this.setMedia()
|
||||
this.$store.dispatch('setCurrent', this.attachment)
|
||||
}
|
||||
},
|
||||
toggleHidden (event) {
|
||||
if (this.$store.state.config.useOneClickNsfw && !this.showHidden) {
|
||||
this.openModal(event)
|
||||
return
|
||||
}
|
||||
if (this.img && !this.preloadImage) {
|
||||
if (this.img.onload) {
|
||||
this.img.onload()
|
||||
|
|
@ -64,23 +88,6 @@ const Attachment = {
|
|||
} else {
|
||||
this.showHidden = !this.showHidden
|
||||
}
|
||||
},
|
||||
onVideoDataLoad (e) {
|
||||
if (typeof e.srcElement.webkitAudioDecodedByteCount !== 'undefined') {
|
||||
// non-zero if video has audio track
|
||||
if (e.srcElement.webkitAudioDecodedByteCount > 0) {
|
||||
this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
|
||||
}
|
||||
} else if (typeof e.srcElement.mozHasAudio !== 'undefined') {
|
||||
// true if video has audio track
|
||||
if (e.srcElement.mozHasAudio) {
|
||||
this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
|
||||
}
|
||||
} else if (typeof e.srcElement.audioTracks !== 'undefined') {
|
||||
if (e.srcElement.audioTracks.length > 0) {
|
||||
this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,44 @@
|
|||
<template>
|
||||
<div v-if="size==='hide'">
|
||||
<a class="placeholder" v-if="type !== 'html'" target="_blank" :href="attachment.url">[{{nsfw ? "NSFW/" : ""}}{{type.toUpperCase()}}]</a>
|
||||
<div v-if="usePlaceHolder" @click="openModal">
|
||||
<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, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}" v-show="!isEmpty">
|
||||
<a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()">
|
||||
<img :key="nsfwImage" :src="nsfwImage"/>
|
||||
<div
|
||||
v-else class="attachment"
|
||||
:class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}"
|
||||
v-show="!isEmpty"
|
||||
>
|
||||
<a class="image-attachment" v-if="hidden" :href="attachment.url" @click.prevent="toggleHidden">
|
||||
<img class="nsfw" :key="nsfwImage" :src="nsfwImage" :class="{'small': isSmall}"/>
|
||||
<i v-if="type === 'video'" class="play-icon icon-play-circled"></i>
|
||||
</a>
|
||||
<div class="hider" v-if="nsfw && hideNsfwLocal && !hidden">
|
||||
<a href="#" @click.prevent="toggleHidden()">Hide</a>
|
||||
<a href="#" @click.prevent="toggleHidden">Hide</a>
|
||||
</div>
|
||||
<a v-if="type === 'image' && (!hidden || preloadImage)" class="image-attachment" :class="{'hidden': hidden && preloadImage}" :href="attachment.url" target="_blank" :title="attachment.description">
|
||||
<StillImage :class="{'small': isSmall}" :referrerpolicy="referrerpolicy" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
|
||||
|
||||
<a v-if="type === 'image' && (!hidden || preloadImage)"
|
||||
@click="openModal"
|
||||
class="image-attachment"
|
||||
:class="{'hidden': hidden && preloadImage }"
|
||||
:href="attachment.url" target="_blank"
|
||||
:title="attachment.description"
|
||||
>
|
||||
<StillImage :referrerpolicy="referrerpolicy" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
|
||||
</a>
|
||||
|
||||
<video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" @loadeddata="onVideoDataLoad" :src="attachment.url" controls :loop="loopVideo" playsinline></video>
|
||||
<a class="video-container"
|
||||
@click="openModal"
|
||||
v-if="type === 'video' && !hidden"
|
||||
:class="{'small': isSmall}"
|
||||
:href="allowPlay ? undefined : attachment.url"
|
||||
>
|
||||
<VideoAttachment class="video" :attachment="attachment" :controls="allowPlay" />
|
||||
<i v-if="!allowPlay" class="play-icon icon-play-circled"></i>
|
||||
</a>
|
||||
|
||||
<audio v-if="type === 'audio'" :src="attachment.url" controls></audio>
|
||||
|
||||
|
|
@ -40,12 +65,17 @@
|
|||
|
||||
.attachment.media-upload-container {
|
||||
flex: 0 0 auto;
|
||||
max-height: 300px;
|
||||
max-height: 200px;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
video {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
margin-right: 0.5em;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.nsfw-placeholder {
|
||||
|
|
@ -56,17 +86,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.small-attachment {
|
||||
&.image, &.video {
|
||||
max-width: 35%;
|
||||
}
|
||||
max-height: 100px;
|
||||
}
|
||||
|
||||
.attachment {
|
||||
position: relative;
|
||||
flex: 1 0 30%;
|
||||
margin: 0.5em 0.7em 0.6em 0.0em;
|
||||
margin: 0.5em 0.5em 0em 0em;
|
||||
align-self: flex-start;
|
||||
line-height: 0;
|
||||
|
||||
|
|
@ -78,6 +100,28 @@
|
|||
border-color: var(--border, $fallback--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.non-gallery.attachment {
|
||||
&.video {
|
||||
flex: 1 0 40%;
|
||||
}
|
||||
.nsfw {
|
||||
height: 260px;
|
||||
}
|
||||
.small {
|
||||
height: 120px;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.video {
|
||||
height: 260px;
|
||||
display: flex;
|
||||
}
|
||||
video {
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.fullwidth {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
|
@ -86,6 +130,28 @@
|
|||
line-height: 0;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
display: flex;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.play-icon {
|
||||
position: absolute;
|
||||
font-size: 64px;
|
||||
top: calc(50% - 32px);
|
||||
left: calc(50% - 32px);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
text-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.play-icon::before {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.html {
|
||||
flex-basis: 90%;
|
||||
width: 100%;
|
||||
|
|
@ -94,6 +160,7 @@
|
|||
|
||||
.hider {
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
margin: 10px;
|
||||
padding: 5px;
|
||||
background: rgba(230,230,230,0.6);
|
||||
|
|
@ -104,13 +171,7 @@
|
|||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
}
|
||||
|
||||
.small {
|
||||
max-height: 100px;
|
||||
}
|
||||
video {
|
||||
max-height: 500px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +181,7 @@
|
|||
|
||||
img.media-upload {
|
||||
line-height: 0;
|
||||
max-height: 300px;
|
||||
max-height: 200px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
|
|
@ -157,29 +218,20 @@
|
|||
}
|
||||
|
||||
.image-attachment {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.still-image {
|
||||
.nsfw {
|
||||
object-fit: cover;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
62
src/components/follow_list/follow_list.js
Normal file
62
src/components/follow_list/follow_list.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import UserCard from '../user_card/user_card.vue'
|
||||
|
||||
const FollowList = {
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
bottomedOut: false,
|
||||
error: false
|
||||
}
|
||||
},
|
||||
props: ['userId', 'showFollowers'],
|
||||
created () {
|
||||
window.addEventListener('scroll', this.scrollLoad)
|
||||
if (this.entries.length === 0) {
|
||||
this.fetchEntries()
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('scroll', this.scrollLoad)
|
||||
this.$store.dispatch('clearFriendsAndFollowers', this.userId)
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.$store.getters.userById(this.userId)
|
||||
},
|
||||
entries () {
|
||||
return this.showFollowers ? this.user.followers : this.user.friends
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchEntries () {
|
||||
if (!this.loading) {
|
||||
const command = this.showFollowers ? 'addFollowers' : 'addFriends'
|
||||
this.loading = true
|
||||
this.$store.dispatch(command, this.userId).then(entries => {
|
||||
this.error = false
|
||||
this.loading = false
|
||||
this.bottomedOut = entries.length === 0
|
||||
}).catch(() => {
|
||||
this.error = true
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
},
|
||||
scrollLoad (e) {
|
||||
const bodyBRect = document.body.getBoundingClientRect()
|
||||
const height = Math.max(bodyBRect.height, -(bodyBRect.y))
|
||||
if (this.loading === false &&
|
||||
this.bottomedOut === false &&
|
||||
this.$el.offsetHeight > 0 &&
|
||||
(window.innerHeight + window.pageYOffset) >= (height - 750)
|
||||
) {
|
||||
this.fetchEntries()
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
UserCard
|
||||
}
|
||||
}
|
||||
|
||||
export default FollowList
|
||||
33
src/components/follow_list/follow_list.vue
Normal file
33
src/components/follow_list/follow_list.vue
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<div class="follow-list">
|
||||
<user-card
|
||||
v-for="entry in entries"
|
||||
:key="entry.id" :user="entry"
|
||||
:showFollows="true"
|
||||
/>
|
||||
<div class="text-center panel-footer">
|
||||
<a v-if="error" @click="fetchEntries" class="alert error">
|
||||
{{$t('general.generic_error')}}
|
||||
</a>
|
||||
<i v-else-if="loading" class="icon-spin3 animate-spin"/>
|
||||
<span v-else-if="bottomedOut"></span>
|
||||
<a v-else @click="fetchEntries">{{$t('general.more')}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./follow_list.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.follow-list {
|
||||
.panel-footer {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
55
src/components/gallery/gallery.js
Normal file
55
src/components/gallery/gallery.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import Attachment from '../attachment/attachment.vue'
|
||||
import { chunk, last, dropRight } from 'lodash'
|
||||
|
||||
const Gallery = {
|
||||
data: () => ({
|
||||
width: 500
|
||||
}),
|
||||
props: [
|
||||
'attachments',
|
||||
'nsfw',
|
||||
'setMedia'
|
||||
],
|
||||
components: { Attachment },
|
||||
mounted () {
|
||||
this.resize()
|
||||
window.addEventListener('resize', this.resize)
|
||||
},
|
||||
destroyed () {
|
||||
window.removeEventListener('resize', this.resize)
|
||||
},
|
||||
computed: {
|
||||
rows () {
|
||||
if (!this.attachments) {
|
||||
return []
|
||||
}
|
||||
const rows = chunk(this.attachments, 3)
|
||||
if (last(rows).length === 1 && rows.length > 1) {
|
||||
// if 1 attachment on last row -> add it to the previous row instead
|
||||
const lastAttachment = last(rows)[0]
|
||||
const allButLastRow = dropRight(rows)
|
||||
last(allButLastRow).push(lastAttachment)
|
||||
return allButLastRow
|
||||
}
|
||||
return rows
|
||||
},
|
||||
rowHeight () {
|
||||
return itemsPerRow => ({ 'height': `${(this.width / (itemsPerRow + 0.6))}px` })
|
||||
},
|
||||
useContainFit () {
|
||||
return this.$store.state.config.useContainFit
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
resize () {
|
||||
// Quick optimization to make resizing not always trigger state change,
|
||||
// only update attachment size in 10px steps
|
||||
const width = Math.floor(this.$el.getBoundingClientRect().width / 10) * 10
|
||||
if (this.width !== width) {
|
||||
this.width = width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Gallery
|
||||
63
src/components/gallery/gallery.vue
Normal file
63
src/components/gallery/gallery.vue
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<template>
|
||||
<div ref="galleryContainer" style="width: 100%;">
|
||||
<div class="gallery-row" v-for="row in rows" :style="rowHeight(row.length)" :class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }">
|
||||
<attachment
|
||||
v-for="attachment in row"
|
||||
:setMedia="setMedia"
|
||||
:nsfw="nsfw"
|
||||
:attachment="attachment"
|
||||
:allowPlay="false"
|
||||
:key="attachment.id"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src='./gallery.js'></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.gallery-row {
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-content: stretch;
|
||||
flex-grow: 1;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.25em;
|
||||
|
||||
.attachments, .attachment {
|
||||
margin: 0 0.5em 0 0;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
// to make failed images a bit more noticeable on chromium
|
||||
min-width: 2em;
|
||||
}
|
||||
|
||||
.image-attachment {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.contain-fit {
|
||||
img, video {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
&.cover-fit {
|
||||
img, video {
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
21
src/components/link-preview/link-preview.js
Normal file
21
src/components/link-preview/link-preview.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
const LinkPreview = {
|
||||
name: 'LinkPreview',
|
||||
props: [
|
||||
'card',
|
||||
'size',
|
||||
'nsfw'
|
||||
],
|
||||
computed: {
|
||||
useImage () {
|
||||
// Currently BE shoudn't give cards if tagged NSFW, this is a bit paranoid
|
||||
// as it makes sure to hide the image if somehow NSFW tagged preview can
|
||||
// exist.
|
||||
return this.card.image && !this.nsfw && this.size !== 'hide'
|
||||
},
|
||||
useDescription () {
|
||||
return this.card.description && /\S/.test(this.card.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default LinkPreview
|
||||
79
src/components/link-preview/link-preview.vue
Normal file
79
src/components/link-preview/link-preview.vue
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<div>
|
||||
<a class="link-preview-card" :href="card.url" target="_blank" rel="noopener">
|
||||
<div class="card-image" :class="{ 'small-image': size === 'small' }" v-if="useImage">
|
||||
<img :src="card.image"></img>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<span class="card-host faint">{{ card.provider_name }}</span>
|
||||
<h4 class="card-title">{{ card.title }}</h4>
|
||||
<p class="card-description" v-if="useDescription">{{ card.description }}</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./link-preview.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.link-preview-card {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
// TODO: clean up the random margins in attachments, this makes preview line
|
||||
// up with attachments...
|
||||
margin-right: 0.5em;
|
||||
|
||||
.card-image {
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
max-width: 25%;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: $fallback--attachmentRadius;
|
||||
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
|
||||
}
|
||||
}
|
||||
|
||||
.small-image {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
max-height: 100%;
|
||||
margin: 0.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-host {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
margin: 0.5em 0 0 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
word-break: break-word;
|
||||
line-height: 1.2em;
|
||||
// cap description at 3 lines, the 1px is to clean up some stray pixels
|
||||
// TODO: fancier fade-out at the bottom to show off that it's too long?
|
||||
max-height: calc(1.2em * 3 - 1px);
|
||||
}
|
||||
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: $fallback--attachmentRadius;
|
||||
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
|
||||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -22,19 +22,29 @@ const LoginForm = {
|
|||
oauth: this.$store.state.oauth,
|
||||
instance: this.$store.state.instance.server
|
||||
}
|
||||
this.clearError()
|
||||
oauthApi.getOrCreateApp(data).then((app) => {
|
||||
oauthApi.getTokenWithCredentials(
|
||||
{
|
||||
app,
|
||||
instance: data.instance,
|
||||
username: this.user.username,
|
||||
password: this.user.password})
|
||||
.then((result) => {
|
||||
this.$store.commit('setToken', result.access_token)
|
||||
this.$store.dispatch('loginUser', result.access_token)
|
||||
this.$router.push({name: 'friends'})
|
||||
})
|
||||
password: this.user.password
|
||||
}
|
||||
).then((result) => {
|
||||
if (result.error) {
|
||||
this.authError = result.error
|
||||
this.user.password = ''
|
||||
return
|
||||
}
|
||||
this.$store.commit('setToken', result.access_token)
|
||||
this.$store.dispatch('loginUser', result.access_token)
|
||||
this.$router.push({name: 'friends'})
|
||||
})
|
||||
})
|
||||
},
|
||||
clearError () {
|
||||
this.authError = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="authError" class='form-group'>
|
||||
<div class='alert error'>
|
||||
{{authError}}
|
||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -48,10 +55,6 @@
|
|||
width: 10em;
|
||||
}
|
||||
|
||||
.error {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.register {
|
||||
flex: 1 1;
|
||||
}
|
||||
|
|
@ -64,4 +67,14 @@
|
|||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.login {
|
||||
.error {
|
||||
text-align: center;
|
||||
|
||||
animation-name: shakeError;
|
||||
animation-duration: 0.4s;
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
38
src/components/media_modal/media_modal.js
Normal file
38
src/components/media_modal/media_modal.js
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import StillImage from '../still-image/still-image.vue'
|
||||
import VideoAttachment from '../video_attachment/video_attachment.vue'
|
||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||
|
||||
const MediaModal = {
|
||||
components: {
|
||||
StillImage,
|
||||
VideoAttachment
|
||||
},
|
||||
computed: {
|
||||
showing () {
|
||||
return this.$store.state.mediaViewer.activated
|
||||
},
|
||||
currentIndex () {
|
||||
return this.$store.state.mediaViewer.currentIndex
|
||||
},
|
||||
currentMedia () {
|
||||
return this.$store.state.mediaViewer.media[this.currentIndex]
|
||||
},
|
||||
type () {
|
||||
return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
document.addEventListener('keyup', e => {
|
||||
if (e.keyCode === 27 && this.showing) { // escape
|
||||
this.hide()
|
||||
}
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
hide () {
|
||||
this.$store.dispatch('closeMediaViewer')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MediaModal
|
||||
38
src/components/media_modal/media_modal.vue
Normal file
38
src/components/media_modal/media_modal.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<div class="modal-view" v-if="showing" @click.prevent="hide">
|
||||
<img class="modal-image" v-if="type === 'image'" :src="currentMedia.url"></img>
|
||||
<VideoAttachment
|
||||
class="modal-image"
|
||||
v-if="type === 'video'"
|
||||
:attachment="currentMedia"
|
||||
:controls="true"
|
||||
@click.stop.native="">
|
||||
</VideoAttachment>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./media_modal.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.modal-view {
|
||||
z-index: 1000;
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.modal-image {
|
||||
max-width: 90%;
|
||||
max-height: 90%;
|
||||
box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -3,19 +3,10 @@ import statusPosterService from '../../services/status_poster/status_poster.serv
|
|||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||
|
||||
const mediaUpload = {
|
||||
mounted () {
|
||||
const input = this.$el.querySelector('input')
|
||||
|
||||
input.addEventListener('change', ({target}) => {
|
||||
for (var i = 0; i < target.files.length; i++) {
|
||||
let file = target.files[i]
|
||||
this.uploadFile(file)
|
||||
}
|
||||
})
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
uploading: false
|
||||
uploading: false,
|
||||
uploadReady: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -56,6 +47,18 @@ const mediaUpload = {
|
|||
} else {
|
||||
e.dataTransfer.dropEffect = 'none'
|
||||
}
|
||||
},
|
||||
clearFile () {
|
||||
this.uploadReady = false
|
||||
this.$nextTick(() => {
|
||||
this.uploadReady = true
|
||||
})
|
||||
},
|
||||
change ({target}) {
|
||||
for (var i = 0; i < target.files.length; i++) {
|
||||
let file = target.files[i]
|
||||
this.uploadFile(file)
|
||||
}
|
||||
}
|
||||
},
|
||||
props: [
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<label class="btn btn-default" :title="$t('tool_tip.media_upload')">
|
||||
<i class="icon-spin4 animate-spin" v-if="uploading"></i>
|
||||
<i class="icon-upload" v-if="!uploading"></i>
|
||||
<input type="file" style="position: fixed; top: -100em" multiple="true"></input>
|
||||
<input type="file" v-if="uploadReady" @change="change" style="position: fixed; top: -100em" multiple="true"></input>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@
|
|||
|
||||
.nav-panel .panel {
|
||||
overflow: hidden;
|
||||
box-shadow: var(--panelShadow);
|
||||
}
|
||||
.nav-panel ul {
|
||||
list-style: none;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Status from '../status/status.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
|
@ -13,7 +13,7 @@ const Notification = {
|
|||
},
|
||||
props: [ 'notification' ],
|
||||
components: {
|
||||
Status, StillImage, UserCardContent
|
||||
Status, UserAvatar, UserCardContent
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
|
||||
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
|
||||
<a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
|
||||
<StillImage class='avatar-compact' :class="{'better-shadow': betterShadow}" :src="notification.action.user.profile_image_url_original"/>
|
||||
<UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
|
||||
</a>
|
||||
<div class='notification-right'>
|
||||
<div class="usercard notification-usercard" v-if="userExpanded">
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ const Notifications = {
|
|||
|
||||
notificationsFetcher.startFetching({ store, credentials })
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
bottomedOut: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
notifications () {
|
||||
return notificationsFromStore(this.$store)
|
||||
|
|
@ -28,6 +33,9 @@ const Notifications = {
|
|||
},
|
||||
unseenCount () {
|
||||
return this.unseenNotifications.length
|
||||
},
|
||||
loading () {
|
||||
return this.$store.state.statuses.notifications.loading
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
@ -49,10 +57,16 @@ const Notifications = {
|
|||
fetchOlderNotifications () {
|
||||
const store = this.$store
|
||||
const credentials = store.state.users.currentUser.credentials
|
||||
store.commit('setNotificationsLoading', { value: true })
|
||||
notificationsFetcher.fetchAndUpdate({
|
||||
store,
|
||||
credentials,
|
||||
older: true
|
||||
}).then(notifs => {
|
||||
store.commit('setNotificationsLoading', { value: false })
|
||||
if (notifs.length === 0) {
|
||||
this.bottomedOut = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,26 +36,7 @@
|
|||
border-color: $fallback--border;
|
||||
border-color: var(--border, $fallback--border);
|
||||
|
||||
.avatar-compact {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
box-shadow: var(--avatarStatusShadow);
|
||||
border-radius: $fallback--avatarAltRadius;
|
||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||
overflow: hidden;
|
||||
line-height: 0;
|
||||
|
||||
&.better-shadow {
|
||||
box-shadow: var(--avatarStatusShadowInset);
|
||||
filter: var(--avatarStatusShadowFilter)
|
||||
}
|
||||
|
||||
&.animated::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .animated.avatar-compact {
|
||||
&:hover .animated.avatar {
|
||||
canvas {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,15 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<a href="#" v-on:click.prevent='fetchOlderNotifications()' v-if="!notifications.loading">
|
||||
<div v-if="bottomedOut" class="new-status-notification text-center panel-footer faint">
|
||||
{{$t('notifications.no_more_notifications')}}
|
||||
</div>
|
||||
<a v-else-if="!loading" href="#" v-on:click.prevent="fetchOlderNotifications()">
|
||||
<div class="new-status-notification text-center panel-footer">{{$t('notifications.load_older')}}</div>
|
||||
</a>
|
||||
<div class="new-status-notification text-center panel-footer" v-else>...</div>
|
||||
<div v-else class="new-status-notification text-center panel-footer">
|
||||
<i class="icon-spin3 animate-spin"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const buildMentionsString = ({user, attentions}, currentUser) => {
|
|||
return `@${attention.screen_name}`
|
||||
})
|
||||
|
||||
return mentions.join(' ') + ' '
|
||||
return mentions.length > 0 ? mentions.join(' ') + ' ' : ''
|
||||
}
|
||||
|
||||
const PostStatusForm = {
|
||||
|
|
@ -166,6 +166,11 @@ const PostStatusForm = {
|
|||
},
|
||||
formattingOptionsEnabled () {
|
||||
return this.$store.state.instance.formattingOptionsEnabled
|
||||
},
|
||||
defaultPostContentType () {
|
||||
return typeof this.$store.state.config.postContentType === 'undefined'
|
||||
? this.$store.state.instance.postContentType
|
||||
: this.$store.state.config.postContentType
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -247,6 +252,7 @@ const PostStatusForm = {
|
|||
files: [],
|
||||
visibility: newStatus.visibility
|
||||
}
|
||||
this.$refs.mediaUpload.clearFile()
|
||||
this.$emit('posted')
|
||||
let el = this.$el.querySelector('textarea')
|
||||
el.style.height = 'auto'
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@
|
|||
<div class="visibility-tray">
|
||||
<span class="text-format" v-if="formattingOptionsEnabled">
|
||||
<label for="post-content-type" class="select">
|
||||
<select id="post-content-type" v-model="newStatus.contentType" class="form-control">
|
||||
<select id="post-content-type" v-model="defaultPostContentType" class="form-control">
|
||||
<option value="text/plain">{{$t('post_status.content_type.plain_text')}}</option>
|
||||
<option value="text/html">HTML</option>
|
||||
<option value="text/markdown">Markdown</option>
|
||||
|
|
@ -64,7 +64,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class='form-bottom'>
|
||||
<media-upload @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
|
||||
<media-upload ref="mediaUpload" @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
|
||||
|
||||
<p v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p>
|
||||
<p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p>
|
||||
|
|
|
|||
|
|
@ -147,24 +147,6 @@ $validations-cRed: #f04124;
|
|||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
@keyframes shakeError {
|
||||
0% {
|
||||
transform: translateX(0); }
|
||||
15% {
|
||||
transform: translateX(0.375rem); }
|
||||
30% {
|
||||
transform: translateX(-0.375rem); }
|
||||
45% {
|
||||
transform: translateX(0.375rem); }
|
||||
60% {
|
||||
transform: translateX(-0.375rem); }
|
||||
75% {
|
||||
transform: translateX(0.375rem); }
|
||||
90% {
|
||||
transform: translateX(-0.375rem); }
|
||||
100% {
|
||||
transform: translateX(0); } }
|
||||
|
||||
.form-group--error {
|
||||
animation-name: shakeError;
|
||||
animation-duration: .6s;
|
||||
|
|
@ -215,7 +197,7 @@ $validations-cRed: #f04124;
|
|||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 959px) {
|
||||
@media all and (max-width: 800px) {
|
||||
.registration-form .container {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-env browser */
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
|
||||
import { filter, trim } from 'lodash'
|
||||
|
|
@ -13,6 +13,7 @@ const settings = {
|
|||
hideAttachmentsLocal: user.hideAttachments,
|
||||
hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
|
||||
hideNsfwLocal: user.hideNsfw,
|
||||
useOneClickNsfw: user.useOneClickNsfw,
|
||||
hideISPLocal: user.hideISP,
|
||||
preloadImage: user.preloadImage,
|
||||
|
||||
|
|
@ -29,7 +30,6 @@ const settings = {
|
|||
notificationVisibilityLocal: user.notificationVisibility,
|
||||
replyVisibilityLocal: user.replyVisibility,
|
||||
loopVideoLocal: user.loopVideo,
|
||||
loopVideoSilentOnlyLocal: user.loopVideoSilentOnly,
|
||||
muteWordsString: user.muteWords.join('\n'),
|
||||
autoLoadLocal: user.autoLoad,
|
||||
streamingLocal: user.streaming,
|
||||
|
|
@ -46,6 +46,11 @@ const settings = {
|
|||
: user.subjectLineBehavior,
|
||||
subjectLineBehaviorDefault: instance.subjectLineBehavior,
|
||||
|
||||
postContentTypeLocal: typeof user.postContentType === 'undefined'
|
||||
? instance.postContentType
|
||||
: user.postContentType,
|
||||
postContentTypeDefault: instance.postContentType,
|
||||
|
||||
alwaysShowSubjectInputLocal: typeof user.alwaysShowSubjectInput === 'undefined'
|
||||
? instance.alwaysShowSubjectInput
|
||||
: user.alwaysShowSubjectInput,
|
||||
|
|
@ -58,13 +63,16 @@ const settings = {
|
|||
|
||||
stopGifs: user.stopGifs,
|
||||
webPushNotificationsLocal: user.webPushNotifications,
|
||||
loopVideoSilentOnlyLocal: user.loopVideosSilentOnly,
|
||||
loopSilentAvailable:
|
||||
// Firefox
|
||||
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
|
||||
// Chrome-likes
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
|
||||
// Future spec, still not supported in Nightly 63 as of 08/2018
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
|
||||
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
|
||||
playVideosInModal: user.playVideosInModal,
|
||||
useContainFit: user.useContainFit
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
@ -96,6 +104,9 @@ const settings = {
|
|||
hideNsfwLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
|
||||
},
|
||||
useOneClickNsfw (value) {
|
||||
this.$store.dispatch('setOption', { name: 'useOneClickNsfw', value })
|
||||
},
|
||||
preloadImage (value) {
|
||||
this.$store.dispatch('setOption', { name: 'preloadImage', value })
|
||||
},
|
||||
|
|
@ -151,12 +162,21 @@ const settings = {
|
|||
subjectLineBehaviorLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'subjectLineBehavior', value })
|
||||
},
|
||||
postContentTypeLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'postContentType', value })
|
||||
},
|
||||
stopGifs (value) {
|
||||
this.$store.dispatch('setOption', { name: 'stopGifs', value })
|
||||
},
|
||||
webPushNotificationsLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'webPushNotifications', value })
|
||||
if (value) this.$store.dispatch('registerPushNotifications')
|
||||
},
|
||||
playVideosInModal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'playVideosInModal', value })
|
||||
},
|
||||
useContainFit (value) {
|
||||
this.$store.dispatch('setOption', { name: 'useContainFit', value })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,28 @@
|
|||
</label>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div>
|
||||
{{$t('settings.post_status_content_type')}}
|
||||
<label for="postContentType" class="select">
|
||||
<select id="postContentType" v-model="postContentTypeLocal">
|
||||
<option value="text/plain">
|
||||
{{$t('settings.status_content_type_plain')}}
|
||||
{{postContentTypeDefault == 'text/plain' ? $t('settings.instance_default_simple') : ''}}
|
||||
</option>
|
||||
<option value="text/html">
|
||||
HTML
|
||||
{{postContentTypeDefault == 'text/html' ? $t('settings.instance_default_simple') : ''}}
|
||||
</option>
|
||||
<option value="text/markdown">
|
||||
Markdown
|
||||
{{postContentTypeDefault == 'text/markdown' ? $t('settings.instance_default_simple') : ''}}
|
||||
</option>
|
||||
</select>
|
||||
<i class="icon-down-open"/>
|
||||
</label>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
@ -123,6 +145,10 @@
|
|||
<input :disabled="!hideNsfwLocal" type="checkbox" id="preloadImage" v-model="preloadImage">
|
||||
<label for="preloadImage">{{$t('settings.preload_images')}}</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" id="useOneClickNsfw" v-model="useOneClickNsfw">
|
||||
<label for="useOneClickNsfw">{{$t('settings.use_one_click_nsfw')}}</label>
|
||||
</li>
|
||||
</ul>
|
||||
<li>
|
||||
<input type="checkbox" id="stopGifs" v-model="stopGifs">
|
||||
|
|
@ -141,6 +167,14 @@
|
|||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" id="playVideosInModal" v-model="playVideosInModal">
|
||||
<label for="playVideosInModal">{{$t('settings.play_videos_in_modal')}}</label>
|
||||
</li>
|
||||
<li>
|
||||
<input type="checkbox" id="useContainFit" v-model="useContainFit">
|
||||
<label for="useContainFit">{{$t('settings.use_contain_fit')}}</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ const SideDrawer = {
|
|||
},
|
||||
suggestionsEnabled () {
|
||||
return this.$store.state.instance.suggestionsEnabled
|
||||
},
|
||||
logo () {
|
||||
return this.$store.state.instance.logo
|
||||
},
|
||||
sitename () {
|
||||
return this.$store.state.instance.name
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
|||
|
|
@ -8,8 +8,11 @@
|
|||
@touchmove="touchMove"
|
||||
>
|
||||
<div class="side-drawer-heading" @click="toggleDrawer">
|
||||
<user-card-content :user="currentUser" :switcher="false" :hideBio="true" v-if="currentUser">
|
||||
</user-card-content>
|
||||
<user-card-content :user="currentUser" :switcher="false" :hideBio="true" v-if="currentUser"/>
|
||||
<div class="side-drawer-logo-wrapper" v-else>
|
||||
<img :src="logo"/>
|
||||
<span>{{sitename}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ul>
|
||||
<li v-if="currentUser" @click="toggleDrawer">
|
||||
|
|
@ -128,7 +131,7 @@
|
|||
|
||||
.side-drawer {
|
||||
overflow-x: hidden;
|
||||
transition: 0.5s;
|
||||
transition: 0.35s;
|
||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||
margin: 0 0 0 -100px;
|
||||
padding: 0 0 1em 100px;
|
||||
|
|
@ -141,12 +144,30 @@
|
|||
background-color: var(--bg, $fallback--bg);
|
||||
}
|
||||
|
||||
.side-drawer-logo-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.85em;
|
||||
|
||||
img {
|
||||
flex: none;
|
||||
height: 50px;
|
||||
margin-right: 0.85em;
|
||||
}
|
||||
|
||||
span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.side-drawer-click-outside-closed {
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
.side-drawer-closed {
|
||||
margin: 0 0 0 calc(-100% - 100px);
|
||||
transform: translate(-100%);
|
||||
}
|
||||
|
||||
.side-drawer-heading {
|
||||
|
|
@ -154,7 +175,6 @@
|
|||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
display: flex;
|
||||
min-height: 7em;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,10 +4,14 @@ 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 StillImage from '../still-image/still-image.vue'
|
||||
import { filter, find } from 'lodash'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import Gallery from '../gallery/gallery.vue'
|
||||
import LinkPreview from '../link-preview/link-preview.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import fileType from 'src/services/file_type/file_type.service'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import { mentionMatchesUrl } from 'src/services/mention_matcher/mention_matcher.js'
|
||||
import { filter, find } from 'lodash'
|
||||
|
||||
const Status = {
|
||||
name: 'Status',
|
||||
|
|
@ -31,11 +35,13 @@ const Status = {
|
|||
userExpanded: false,
|
||||
preview: null,
|
||||
showPreview: false,
|
||||
showingTall: false,
|
||||
showingTall: this.inConversation && this.focused,
|
||||
showingLongSubject: false,
|
||||
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
|
||||
? !this.$store.state.instance.collapseMessageWithSubject
|
||||
: !this.$store.state.config.collapseMessageWithSubject,
|
||||
betterShadow: this.$store.state.interface.browserSupport.cssFilter
|
||||
betterShadow: this.$store.state.interface.browserSupport.cssFilter,
|
||||
maxAttachments: 9
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -78,12 +84,13 @@ const Status = {
|
|||
},
|
||||
replyProfileLink () {
|
||||
if (this.isReply) {
|
||||
return this.generateUserProfileLink(this.status.in_reply_to_status_id, this.replyToName)
|
||||
return this.generateUserProfileLink(this.status.in_reply_to_user_id, this.replyToName)
|
||||
}
|
||||
},
|
||||
retweet () { return !!this.statusoid.retweeted_status },
|
||||
retweeter () { return this.statusoid.user.name },
|
||||
retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name },
|
||||
retweeterHtml () { return this.statusoid.user.name_html },
|
||||
retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },
|
||||
status () {
|
||||
if (this.retweet) {
|
||||
return this.statusoid.retweeted_status
|
||||
|
|
@ -124,8 +131,11 @@ const Status = {
|
|||
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
|
||||
return lengthScore > 20
|
||||
},
|
||||
longSubject () {
|
||||
return this.status.summary.length > 900
|
||||
},
|
||||
isReply () {
|
||||
return !!this.status.in_reply_to_status_id
|
||||
return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
|
||||
},
|
||||
replyToName () {
|
||||
const user = this.$store.state.users.usersObject[this.status.in_reply_to_user_id]
|
||||
|
|
@ -178,7 +188,7 @@ const Status = {
|
|||
return this.tallStatus
|
||||
},
|
||||
showingMore () {
|
||||
return this.showingTall || (this.status.summary && this.expandingSubject)
|
||||
return (this.tallStatus && this.showingTall) || (this.status.summary && this.expandingSubject)
|
||||
},
|
||||
nsfwClickthrough () {
|
||||
if (!this.status.nsfw) {
|
||||
|
|
@ -205,12 +215,31 @@ const Status = {
|
|||
},
|
||||
attachmentSize () {
|
||||
if ((this.$store.state.config.hideAttachments && !this.inConversation) ||
|
||||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation)) {
|
||||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation) ||
|
||||
(this.status.attachments.length > this.maxAttachments)) {
|
||||
return 'hide'
|
||||
} else if (this.compact) {
|
||||
return 'small'
|
||||
}
|
||||
return 'normal'
|
||||
},
|
||||
galleryTypes () {
|
||||
if (this.attachmentSize === 'hide') {
|
||||
return []
|
||||
}
|
||||
return this.$store.state.config.playVideosInModal
|
||||
? ['image', 'video']
|
||||
: ['image']
|
||||
},
|
||||
galleryAttachments () {
|
||||
return this.status.attachments.filter(
|
||||
file => fileType.fileMatchesSomeType(this.galleryTypes, file)
|
||||
)
|
||||
},
|
||||
nonGalleryAttachments () {
|
||||
return this.status.attachments.filter(
|
||||
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
|
||||
)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
|
@ -220,7 +249,9 @@ const Status = {
|
|||
DeleteButton,
|
||||
PostStatusForm,
|
||||
UserCardContent,
|
||||
StillImage
|
||||
UserAvatar,
|
||||
Gallery,
|
||||
LinkPreview
|
||||
},
|
||||
methods: {
|
||||
visibilityIcon (visibility) {
|
||||
|
|
@ -235,11 +266,23 @@ const Status = {
|
|||
return 'icon-globe'
|
||||
}
|
||||
},
|
||||
linkClicked ({target}) {
|
||||
linkClicked (event) {
|
||||
let { target } = event
|
||||
if (target.tagName === 'SPAN') {
|
||||
target = target.parentNode
|
||||
}
|
||||
if (target.tagName === 'A') {
|
||||
if (target.className.match(/mention/)) {
|
||||
const href = target.getAttribute('href')
|
||||
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
||||
if (attn) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
const link = this.generateUserProfileLink(attn.id, attn.screen_name)
|
||||
this.$router.push(link)
|
||||
return
|
||||
}
|
||||
}
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
|
|
@ -264,11 +307,11 @@ const Status = {
|
|||
toggleShowMore () {
|
||||
if (this.showingTall) {
|
||||
this.showingTall = false
|
||||
} else if (this.expandingSubject) {
|
||||
} else if (this.expandingSubject && this.status.summary) {
|
||||
this.expandingSubject = false
|
||||
} else if (this.hideTallStatus) {
|
||||
this.showingTall = true
|
||||
} else if (this.hideSubjectStatus) {
|
||||
} else if (this.hideSubjectStatus && this.status.summary) {
|
||||
this.expandingSubject = true
|
||||
}
|
||||
},
|
||||
|
|
@ -295,6 +338,10 @@ const Status = {
|
|||
},
|
||||
generateUserProfileLink (id, name) {
|
||||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||
},
|
||||
setMedia () {
|
||||
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
||||
return () => this.$store.dispatch('setMedia', attachments)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
@ -302,8 +349,13 @@ const Status = {
|
|||
if (this.status.id === id) {
|
||||
let rect = this.$el.getBoundingClientRect()
|
||||
if (rect.top < 100) {
|
||||
window.scrollBy(0, rect.top - 200)
|
||||
// Post is above screen, match its top to screen top
|
||||
window.scrollBy(0, rect.top - 100)
|
||||
} else if (rect.height >= (window.innerHeight - 50)) {
|
||||
// Post we want to see is taller than screen so match its top to screen top
|
||||
window.scrollBy(0, rect.top - 100)
|
||||
} else if (rect.bottom > window.innerHeight - 50) {
|
||||
// Post is below screen, match its bottom to screen bottom
|
||||
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,12 @@
|
|||
</template>
|
||||
<template v-else>
|
||||
<div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
|
||||
<StillImage v-if="retweet" class='avatar' :class='{ "better-shadow": betterShadow }' :src="statusoid.user.profile_image_url_original"/>
|
||||
<UserAvatar v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
|
||||
<div class="media-body faint">
|
||||
<a v-if="retweeterHtml" :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a>
|
||||
<a v-else :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
|
||||
<span class="user-name">
|
||||
<router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/>
|
||||
<router-link v-else :to="retweeterProfileLink">{{retweeter}}</router-link>
|
||||
</span>
|
||||
<i class='fa icon-retweet retweeted' :title="$t('tool_tip.repeat')"></i>
|
||||
{{$t('timeline.repeated')}}
|
||||
</div>
|
||||
|
|
@ -24,9 +26,9 @@
|
|||
|
||||
<div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet }]" :style="[ userStyle ]" class="media status">
|
||||
<div v-if="!noHeading" class="media-left">
|
||||
<a :href="status.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
|
||||
<StillImage class='avatar' :class="{'avatar-compact': compact, 'better-shadow': betterShadow}" :src="status.user.profile_image_url_original"/>
|
||||
</a>
|
||||
<router-link :to="userProfileLink" @click.stop.prevent.capture.native="toggleUserExpanded">
|
||||
<UserAvatar :compact="compact" :betterShadow="betterShadow" :src="status.user.profile_image_url_original"/>
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="status-body">
|
||||
<div class="usercard media-body" v-if="userExpanded">
|
||||
|
|
@ -85,7 +87,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper">
|
||||
<div class="status-content-wrapper" :class="{ 'tall-status': !showingLongSubject }" v-if="longSubject">
|
||||
<a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="!showingLongSubject" href="#" @click.prevent="showingLongSubject=true">Show more</a>
|
||||
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html"></div>
|
||||
<a v-if="showingLongSubject" href="#" class="status-unhider" @click.prevent="showingLongSubject=false">Show less</a>
|
||||
</div>
|
||||
<div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper" v-else>
|
||||
<a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowMore">Show more</a>
|
||||
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html" v-if="!hideSubjectStatus"></div>
|
||||
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary_html" v-else></div>
|
||||
|
|
@ -93,9 +100,27 @@
|
|||
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">Show less</a>
|
||||
</div>
|
||||
|
||||
<div v-if='status.attachments && !hideSubjectStatus' class='attachments media-body'>
|
||||
<attachment :size="attachmentSize" :status-id="status.id" :nsfw="nsfwClickthrough" :attachment="attachment" v-for="attachment in status.attachments" :key="attachment.id">
|
||||
</attachment>
|
||||
<div v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)" class="attachments media-body">
|
||||
<attachment
|
||||
class="non-gallery"
|
||||
v-for="attachment in nonGalleryAttachments"
|
||||
:size="attachmentSize"
|
||||
:nsfw="nsfwClickthrough"
|
||||
:attachment="attachment"
|
||||
:allowPlay="true"
|
||||
:setMedia="setMedia()"
|
||||
:key="attachment.id"
|
||||
/>
|
||||
<gallery
|
||||
v-if="galleryAttachments.length > 0"
|
||||
:nsfw="nsfwClickthrough"
|
||||
:attachments="galleryAttachments"
|
||||
:setMedia="setMedia()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="status.card && !hideSubjectStatus && !noHeading" class="link-preview media-body">
|
||||
<link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" />
|
||||
</div>
|
||||
|
||||
<div v-if="!noHeading && !noReplyLinks" class='status-actions media-body'>
|
||||
|
|
@ -221,6 +246,11 @@
|
|||
vertical-align: bottom;
|
||||
flex-basis: 100%;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
small {
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
|
@ -262,6 +292,14 @@
|
|||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
& > span {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
& > a:last-child {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
.reply-info {
|
||||
display: flex;
|
||||
|
|
@ -296,11 +334,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.tall-status {
|
||||
position: relative;
|
||||
height: 220px;
|
||||
|
|
@ -309,6 +342,8 @@
|
|||
}
|
||||
|
||||
.tall-status-hider {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
position: absolute;
|
||||
height: 70px;
|
||||
margin-top: 150px;
|
||||
|
|
@ -326,6 +361,8 @@
|
|||
.status-unhider, .cw-status-hider {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.status-content {
|
||||
|
|
@ -383,7 +420,7 @@
|
|||
padding: 0.4em 0.6em 0 0.6em;
|
||||
margin: 0;
|
||||
|
||||
.avatar {
|
||||
.avatar.still-image {
|
||||
border-radius: $fallback--avatarAltRadius;
|
||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||
margin-left: 28px;
|
||||
|
|
@ -467,46 +504,6 @@
|
|||
color: var(--cBlue, $fallback--cBlue);
|
||||
}
|
||||
|
||||
.status .avatar-compact {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
box-shadow: var(--avatarStatusShadow);
|
||||
border-radius: $fallback--avatarAltRadius;
|
||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||
|
||||
&.better-shadow {
|
||||
box-shadow: var(--avatarStatusShadowInset);
|
||||
filter: var(--avatarStatusShadowFilter)
|
||||
}
|
||||
}
|
||||
|
||||
.avatar.still-image {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
box-shadow: var(--avatarStatusShadow);
|
||||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
&.better-shadow {
|
||||
box-shadow: var(--avatarStatusShadowInset);
|
||||
filter: var(--avatarStatusShadowFilter)
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.animated::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.retweeted {
|
||||
}
|
||||
}
|
||||
|
||||
.status:hover .animated.avatar {
|
||||
canvas {
|
||||
display: none;
|
||||
|
|
@ -561,10 +558,10 @@ a.unmute {
|
|||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 960px) {
|
||||
@media all and (max-width: 800px) {
|
||||
.status-el {
|
||||
.retweet-info {
|
||||
.avatar {
|
||||
.avatar.still-image {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
|
@ -573,14 +570,14 @@ a.unmute {
|
|||
max-width: 100%;
|
||||
}
|
||||
|
||||
.status .avatar {
|
||||
.status .avatar.still-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.status .avatar-compact {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
&.avatar-compact {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ const StillImage = {
|
|||
props: [
|
||||
'src',
|
||||
'referrerpolicy',
|
||||
'mimetype'
|
||||
'mimetype',
|
||||
'imageLoadError'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
|
@ -23,6 +24,9 @@ const StillImage = {
|
|||
canvas.width = width
|
||||
canvas.height = height
|
||||
canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height)
|
||||
},
|
||||
onError () {
|
||||
this.imageLoadError && this.imageLoadError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class='still-image' :class='{ animated: animated }' >
|
||||
<canvas ref="canvas" v-if="animated"></canvas>
|
||||
<img ref="src" :src="src" :referrerpolicy="referrerpolicy" v-on:load="onLoad"/>
|
||||
<img ref="src" :src="src" :referrerpolicy="referrerpolicy" v-on:load="onLoad" @error="onError"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import OpacityInput from '../opacity_input/opacity_input.vue'
|
|||
import ShadowControl from '../shadow_control/shadow_control.vue'
|
||||
import FontControl from '../font_control/font_control.vue'
|
||||
import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||
import Preview from './preview.vue'
|
||||
import ExportImport from '../export_import/export_import.vue'
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import './tab_switcher.scss'
|
|||
|
||||
export default Vue.component('tab-switcher', {
|
||||
name: 'TabSwitcher',
|
||||
props: ['renderOnlyFocused'],
|
||||
data () {
|
||||
return {
|
||||
active: this.$slots.default.findIndex(_ => _.tag)
|
||||
|
|
@ -44,11 +45,12 @@ export default Vue.component('tab-switcher', {
|
|||
const contents = this.$slots.default.map((slot, index) => {
|
||||
if (!slot.tag) return
|
||||
const active = index === this.active
|
||||
return (
|
||||
<div class={active ? 'active' : 'hidden'}>
|
||||
{slot}
|
||||
</div>
|
||||
)
|
||||
if (this.renderOnlyFocused) {
|
||||
return active
|
||||
? <div class="active">{slot}</div>
|
||||
: <div class="hidden"></div>
|
||||
}
|
||||
return <div class={active ? 'active' : 'hidden' }>{slot}</div>
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
@ -16,7 +16,8 @@ const Timeline = {
|
|||
data () {
|
||||
return {
|
||||
paused: false,
|
||||
unfocused: false
|
||||
unfocused: false,
|
||||
bottomedOut: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -95,7 +96,12 @@ const Timeline = {
|
|||
showImmediately: true,
|
||||
userId: this.userId,
|
||||
tag: this.tag
|
||||
}).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false }))
|
||||
}).then(statuses => {
|
||||
store.commit('setLoading', { timeline: this.timelineName, value: false })
|
||||
if (statuses.length === 0) {
|
||||
this.bottomedOut = true
|
||||
}
|
||||
})
|
||||
}, 1000, this),
|
||||
scrollLoad (e) {
|
||||
const bodyBRect = document.body.getBoundingClientRect()
|
||||
|
|
|
|||
|
|
@ -20,10 +20,15 @@
|
|||
</div>
|
||||
</div>
|
||||
<div :class="classes.footer">
|
||||
<a href="#" v-on:click.prevent='fetchOlderStatuses()' v-if="!timeline.loading">
|
||||
<div v-if="bottomedOut" class="new-status-notification text-center panel-footer faint">
|
||||
{{$t('timeline.no_more_statuses')}}
|
||||
</div>
|
||||
<a v-else-if="!timeline.loading" href="#" v-on:click.prevent='fetchOlderStatuses()'>
|
||||
<div class="new-status-notification text-center panel-footer">{{$t('timeline.load_older')}}</div>
|
||||
</a>
|
||||
<div class="new-status-notification text-center panel-footer" v-else>...</div>
|
||||
<div v-else class="new-status-notification text-center panel-footer">
|
||||
<i class="icon-spin3 animate-spin"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
29
src/components/user_avatar/user_avatar.js
Normal file
29
src/components/user_avatar/user_avatar.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import StillImage from '../still-image/still-image.vue'
|
||||
|
||||
const UserAvatar = {
|
||||
props: [
|
||||
'src',
|
||||
'betterShadow',
|
||||
'compact'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
showPlaceholder: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
StillImage
|
||||
},
|
||||
computed: {
|
||||
imgSrc () {
|
||||
return this.showPlaceholder ? '/images/avi.png' : this.src
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
imageLoadError () {
|
||||
this.showPlaceholder = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UserAvatar
|
||||
42
src/components/user_avatar/user_avatar.vue
Normal file
42
src/components/user_avatar/user_avatar.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<StillImage
|
||||
class="avatar"
|
||||
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
|
||||
:src="imgSrc"
|
||||
:imageLoadError="imageLoadError"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script src="./user_avatar.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.avatar.still-image {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
box-shadow: var(--avatarStatusShadow);
|
||||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&.better-shadow {
|
||||
box-shadow: var(--avatarStatusShadowInset);
|
||||
filter: var(--avatarStatusShadowFilter)
|
||||
}
|
||||
|
||||
&.animated::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.avatar-compact {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: $fallback--avatarAltRadius;
|
||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||
import StillImage from '../still-image/still-image.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const UserCard = {
|
||||
|
|
@ -15,7 +15,7 @@ const UserCard = {
|
|||
},
|
||||
components: {
|
||||
UserCardContent,
|
||||
StillImage
|
||||
UserAvatar
|
||||
},
|
||||
computed: {
|
||||
currentUser () { return this.$store.state.users.currentUser }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div class="card">
|
||||
<a href="#">
|
||||
<StillImage @click.prevent="toggleUserExpanded" class="avatar" :src="user.profile_image_url"/>
|
||||
</a>
|
||||
<router-link :to="userProfileLink(user)">
|
||||
<UserAvatar class="avatar" :compact="true" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
|
||||
</router-link>
|
||||
<div class="usercard" v-if="userExpanded">
|
||||
<user-card-content :user="user" :switcher="false"></user-card-content>
|
||||
</div>
|
||||
|
|
@ -69,17 +69,13 @@
|
|||
border-bottom-color: var(--border, $fallback--border);
|
||||
|
||||
.avatar {
|
||||
margin-top: 0.2em;
|
||||
width:32px;
|
||||
height: 32px;
|
||||
border-radius: $fallback--avatarAltRadius;
|
||||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.usercard {
|
||||
width: fill-available;
|
||||
margin: 0.2em 0 0.7em 0;
|
||||
margin: 0.2em 0 0 0.7em;
|
||||
border-radius: $fallback--panelRadius;
|
||||
border-radius: var(--panelRadius, $fallback--panelRadius);
|
||||
border-style: solid;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import StillImage from '../still-image/still-image.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
|
|
@ -79,10 +79,16 @@ export default {
|
|||
set (color) {
|
||||
this.$store.dispatch('setHighlight', { user: this.user.screen_name, color })
|
||||
}
|
||||
},
|
||||
visibleRole () {
|
||||
const validRole = (this.user.role === 'admin' || this.user.role === 'moderator')
|
||||
const showRole = this.isOtherUser || this.user.show_role
|
||||
|
||||
return validRole && showRole && this.user.role
|
||||
}
|
||||
},
|
||||
components: {
|
||||
StillImage
|
||||
UserAvatar
|
||||
},
|
||||
methods: {
|
||||
followUser () {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div class='user-info'>
|
||||
<div class='container'>
|
||||
<router-link :to="userProfileLink(user)">
|
||||
<StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/>
|
||||
<UserAvatar :betterShadow="betterShadow" :src="user.profile_image_url_original"/>
|
||||
</router-link>
|
||||
<div class="name-and-screen-name">
|
||||
<div class="top-line">
|
||||
|
|
@ -19,7 +19,9 @@
|
|||
</div>
|
||||
|
||||
<router-link class='user-screen-name' :to="userProfileLink(user)">
|
||||
<span class="handle">@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
|
||||
<span class="handle">@{{user.screen_name}}
|
||||
<span class="alert staff" v-if="!hideBio && !!visibleRole">{{visibleRole}}</span>
|
||||
</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
|
||||
<span v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
|
@ -107,18 +109,18 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="panel-body profile-panel-body" v-if="!hideBio">
|
||||
<div v-if="!hideUserStatsLocal || switcher" class="user-counts">
|
||||
<div v-if="!hideUserStatsLocal && switcher" class="user-counts">
|
||||
<div class="user-count" v-on:click.prevent="setProfileView('statuses')">
|
||||
<h5>{{ $t('user_card.statuses') }}</h5>
|
||||
<span v-if="!hideUserStatsLocal">{{user.statuses_count}} <br></span>
|
||||
<span>{{user.statuses_count}} <br></span>
|
||||
</div>
|
||||
<div class="user-count" v-on:click.prevent="setProfileView('friends')">
|
||||
<h5>{{ $t('user_card.followees') }}</h5>
|
||||
<span v-if="!hideUserStatsLocal">{{user.friends_count}}</span>
|
||||
<span>{{user.friends_count}}</span>
|
||||
</div>
|
||||
<div class="user-count" v-on:click.prevent="setProfileView('followers')">
|
||||
<h5>{{ $t('user_card.followers') }}</h5>
|
||||
<span v-if="!hideUserStatsLocal">{{user.followers_count}}</span>
|
||||
<span>{{user.followers_count}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p @click.prevent="linkClicked" v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p>
|
||||
|
|
@ -169,23 +171,12 @@
|
|||
max-height: 56px;
|
||||
|
||||
.avatar {
|
||||
border-radius: $fallback--avatarRadius;
|
||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||
flex: 1 0 100%;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
box-shadow: 0px 1px 8px rgba(0,0,0,0.75);
|
||||
box-shadow: var(--avatarShadow);
|
||||
object-fit: cover;
|
||||
|
||||
&.better-shadow {
|
||||
box-shadow: var(--avatarShadowInset);
|
||||
filter: var(--avatarShadowFilter)
|
||||
}
|
||||
|
||||
&.animated::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -258,6 +249,15 @@
|
|||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// TODO use proper colors
|
||||
.staff {
|
||||
text-transform: capitalize;
|
||||
color: $fallback--text;
|
||||
color: var(--btnText, $fallback--text);
|
||||
background-color: $fallback--fg;
|
||||
background-color: var(--btn, $fallback--fg);
|
||||
}
|
||||
}
|
||||
|
||||
.user-meta {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import Timeline from '../timeline/timeline.vue'
|
||||
import FollowList from '../follow_list/follow_list.vue'
|
||||
|
||||
const UserProfile = {
|
||||
created () {
|
||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||
this.$store.commit('clearTimeline', { timeline: 'media' })
|
||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
||||
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
|
||||
this.$store.dispatch('startFetching', ['media', this.fetchBy])
|
||||
this.startFetchFavorites()
|
||||
if (!this.user.id) {
|
||||
this.$store.dispatch('fetchUser', this.fetchBy)
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
this.$store.dispatch('stopFetching', 'user')
|
||||
this.$store.dispatch('stopFetching', 'favorites')
|
||||
this.cleanUp(this.userId)
|
||||
},
|
||||
computed: {
|
||||
timeline () {
|
||||
|
|
@ -23,6 +25,9 @@ const UserProfile = {
|
|||
favorites () {
|
||||
return this.$store.state.statuses.timelines.favorites
|
||||
},
|
||||
media () {
|
||||
return this.$store.state.statuses.timelines.media
|
||||
},
|
||||
userId () {
|
||||
return this.$route.params.id || this.user.id
|
||||
},
|
||||
|
|
@ -30,13 +35,8 @@ const UserProfile = {
|
|||
return this.$route.params.name || this.user.screen_name
|
||||
},
|
||||
isUs () {
|
||||
return this.userId === this.$store.state.users.currentUser.id
|
||||
},
|
||||
friends () {
|
||||
return this.user.friends
|
||||
},
|
||||
followers () {
|
||||
return this.user.followers
|
||||
return this.userId && this.$store.state.users.currentUser.id &&
|
||||
this.userId === this.$store.state.users.currentUser.id
|
||||
},
|
||||
userInStore () {
|
||||
if (this.isExternal) {
|
||||
|
|
@ -58,53 +58,56 @@ const UserProfile = {
|
|||
},
|
||||
isExternal () {
|
||||
return this.$route.name === 'external-user-profile'
|
||||
},
|
||||
followsTabVisible () {
|
||||
return this.isUs || !this.user.hide_follows
|
||||
},
|
||||
followersTabVisible () {
|
||||
return this.isUs || !this.user.hide_followers
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
fetchFollowers () {
|
||||
const id = this.userId
|
||||
this.$store.dispatch('addFollowers', { id })
|
||||
startFetchFavorites () {
|
||||
if (this.isUs) {
|
||||
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
|
||||
}
|
||||
},
|
||||
fetchFriends () {
|
||||
const id = this.userId
|
||||
this.$store.dispatch('addFriends', { id })
|
||||
startUp () {
|
||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
||||
this.$store.dispatch('startFetching', ['media', this.fetchBy])
|
||||
|
||||
this.startFetchFavorites()
|
||||
},
|
||||
cleanUp () {
|
||||
this.$store.dispatch('stopFetching', 'user')
|
||||
this.$store.dispatch('stopFetching', 'favorites')
|
||||
this.$store.dispatch('stopFetching', 'media')
|
||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||
this.$store.commit('clearTimeline', { timeline: 'media' })
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// TODO get rid of this copypasta
|
||||
userName () {
|
||||
if (this.isExternal) {
|
||||
return
|
||||
}
|
||||
this.$store.dispatch('stopFetching', 'user')
|
||||
this.$store.dispatch('stopFetching', 'favorites')
|
||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
||||
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
|
||||
this.cleanUp()
|
||||
this.startUp()
|
||||
},
|
||||
userId () {
|
||||
if (!this.isExternal) {
|
||||
return
|
||||
}
|
||||
this.$store.dispatch('stopFetching', 'user')
|
||||
this.$store.dispatch('stopFetching', 'favorites')
|
||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
||||
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
|
||||
},
|
||||
user () {
|
||||
if (this.user.id && !this.user.followers) {
|
||||
this.fetchFollowers()
|
||||
this.fetchFriends()
|
||||
}
|
||||
this.cleanUp()
|
||||
this.startUp()
|
||||
}
|
||||
},
|
||||
components: {
|
||||
UserCardContent,
|
||||
UserCard,
|
||||
Timeline
|
||||
Timeline,
|
||||
FollowList
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +1,47 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="user.id" class="user-profile panel panel-default">
|
||||
<user-card-content :user="user" :switcher="true" :selected="timeline.viewing"></user-card-content>
|
||||
<tab-switcher>
|
||||
<Timeline :label="$t('user_card.statuses')" :embedded="true" :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="fetchBy"/>
|
||||
<div :label="$t('user_card.followees')">
|
||||
<div v-if="friends">
|
||||
<user-card v-for="friend in friends" :key="friend.id" :user="friend" :showFollows="true"></user-card>
|
||||
</div>
|
||||
<user-card-content
|
||||
:user="user"
|
||||
:switcher="true"
|
||||
:selected="timeline.viewing"
|
||||
/>
|
||||
<tab-switcher :renderOnlyFocused="true">
|
||||
<Timeline
|
||||
:label="$t('user_card.statuses')"
|
||||
:embedded="true"
|
||||
:title="$t('user_profile.timeline_title')"
|
||||
:timeline="timeline"
|
||||
:timeline-name="'user'"
|
||||
:user-id="fetchBy"
|
||||
/>
|
||||
<div :label="$t('user_card.followees')" v-if="followsTabVisible">
|
||||
<FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" />
|
||||
<div class="userlist-placeholder" v-else>
|
||||
<i class="icon-spin3 animate-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div :label="$t('user_card.followers')">
|
||||
<div v-if="followers">
|
||||
<user-card v-for="follower in followers" :key="follower.id" :user="follower" :showFollows="false"></user-card>
|
||||
</div>
|
||||
<div :label="$t('user_card.followers')" v-if="followersTabVisible">
|
||||
<FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" />
|
||||
<div class="userlist-placeholder" v-else>
|
||||
<i class="icon-spin3 animate-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
<Timeline v-if="isUs" :label="$t('user_card.favorites')" :embedded="true" :title="$t('user_profile.favorites_title')" timeline-name="favorites" :timeline="favorites"/>
|
||||
<Timeline
|
||||
:label="$t('user_card.media')"
|
||||
:embedded="true" :title="$t('user_card.media')"
|
||||
timeline-name="media"
|
||||
:timeline="media"
|
||||
:user-id="fetchBy"
|
||||
/>
|
||||
<Timeline
|
||||
v-if="isUs"
|
||||
:label="$t('user_card.favorites')"
|
||||
:embedded="true"
|
||||
:title="$t('user_card.favorites')"
|
||||
timeline-name="favorites"
|
||||
:timeline="favorites"
|
||||
/>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
<div v-else class="panel user-profile-placeholder">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
|
||||
import { unescape } from 'lodash'
|
||||
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||
|
||||
|
|
@ -6,11 +8,14 @@ const UserSettings = {
|
|||
data () {
|
||||
return {
|
||||
newName: this.$store.state.users.currentUser.name,
|
||||
newBio: this.$store.state.users.currentUser.description,
|
||||
newBio: unescape(this.$store.state.users.currentUser.description),
|
||||
newLocked: this.$store.state.users.currentUser.locked,
|
||||
newNoRichText: this.$store.state.users.currentUser.no_rich_text,
|
||||
newDefaultScope: this.$store.state.users.currentUser.default_scope,
|
||||
newHideNetwork: this.$store.state.users.currentUser.hide_network,
|
||||
hideFollows: this.$store.state.users.currentUser.hide_follows,
|
||||
hideFollowers: this.$store.state.users.currentUser.hide_followers,
|
||||
showRole: this.$store.state.users.currentUser.show_role,
|
||||
role: this.$store.state.users.currentUser.role,
|
||||
followList: null,
|
||||
followImportError: false,
|
||||
followsImported: false,
|
||||
|
|
@ -66,7 +71,10 @@ const UserSettings = {
|
|||
/* eslint-disable camelcase */
|
||||
const default_scope = this.newDefaultScope
|
||||
const no_rich_text = this.newNoRichText
|
||||
const hide_network = this.newHideNetwork
|
||||
const hide_follows = this.hideFollows
|
||||
const hide_followers = this.hideFollowers
|
||||
const show_role = this.showRole
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateProfile({
|
||||
|
|
@ -78,7 +86,9 @@ const UserSettings = {
|
|||
/* eslint-disable camelcase */
|
||||
default_scope,
|
||||
no_rich_text,
|
||||
hide_network
|
||||
hide_follows,
|
||||
hide_followers,
|
||||
show_role
|
||||
/* eslint-enable camelcase */
|
||||
}}).then((user) => {
|
||||
if (!user.error) {
|
||||
|
|
@ -233,7 +243,9 @@ const UserSettings = {
|
|||
exportFollows () {
|
||||
this.enableFollowsExport = false
|
||||
this.$store.state.api.backendInteractor
|
||||
.fetchFriends({id: this.$store.state.users.currentUser.id})
|
||||
.exportFriends({
|
||||
id: this.$store.state.users.currentUser.id
|
||||
})
|
||||
.then((friendList) => {
|
||||
this.exportPeople(friendList, 'friends.csv')
|
||||
setTimeout(() => { this.enableFollowsExport = true }, 2000)
|
||||
|
|
|
|||
|
|
@ -30,13 +30,23 @@
|
|||
<label for="account-no-rich-text">{{$t('settings.no_rich_text_description')}}</label>
|
||||
</p>
|
||||
<p>
|
||||
<input type="checkbox" v-model="newHideNetwork" id="account-hide-network">
|
||||
<label for="account-hide-network">{{$t('settings.hide_network_description')}}</label>
|
||||
<input type="checkbox" v-model="hideFollows" id="account-hide-follows">
|
||||
<label for="account-hide-follows">{{$t('settings.hide_follows_description')}}</label>
|
||||
</p>
|
||||
<button :disabled='newName.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
|
||||
<p>
|
||||
<input type="checkbox" v-model="hideFollowers" id="account-hide-followers">
|
||||
<label for="account-hide-followers">{{$t('settings.hide_followers_description')}}</label>
|
||||
</p>
|
||||
<p>
|
||||
<input type="checkbox" v-model="showRole" id="account-show-role">
|
||||
<label for="account-show-role" v-if="role === 'admin'">{{$t('settings.show_admin_badge')}}</label>
|
||||
<label for="account-show-role" v-if="role === 'moderator'">{{$t('settings.show_moderator_badge')}}</label>
|
||||
</p>
|
||||
<button :disabled='newName && newName.length === 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<h2>{{$t('settings.avatar')}}</h2>
|
||||
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
|
||||
<p>{{$t('settings.current_avatar')}}</p>
|
||||
<img :src="user.profile_image_url_original" class="old-avatar"></img>
|
||||
<p>{{$t('settings.set_new_avatar')}}</p>
|
||||
|
|
@ -126,7 +136,7 @@
|
|||
<div class="setting-item">
|
||||
<h2>{{$t('settings.follow_import')}}</h2>
|
||||
<p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
|
||||
<form v-model="followImportForm">
|
||||
<form>
|
||||
<input type="file" ref="followlist" v-on:change="followListChange"></input>
|
||||
</form>
|
||||
<i class=" icon-spin4 animate-spin uploading" v-if="followListUploading"></i>
|
||||
|
|
@ -175,5 +185,9 @@
|
|||
font-size: 1.5em;
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
.name-changer {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
31
src/components/video_attachment/video_attachment.js
Normal file
31
src/components/video_attachment/video_attachment.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
const VideoAttachment = {
|
||||
props: ['attachment', 'controls'],
|
||||
data () {
|
||||
return {
|
||||
loopVideo: this.$store.state.config.loopVideo
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onVideoDataLoad (e) {
|
||||
const target = e.srcElement || e.target
|
||||
if (typeof target.webkitAudioDecodedByteCount !== 'undefined') {
|
||||
// non-zero if video has audio track
|
||||
if (target.webkitAudioDecodedByteCount > 0) {
|
||||
this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
|
||||
}
|
||||
} else if (typeof target.mozHasAudio !== 'undefined') {
|
||||
// true if video has audio track
|
||||
if (target.mozHasAudio) {
|
||||
this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
|
||||
}
|
||||
} else if (typeof target.audioTracks !== 'undefined') {
|
||||
if (target.audioTracks.length > 0) {
|
||||
this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default VideoAttachment
|
||||
11
src/components/video_attachment/video_attachment.vue
Normal file
11
src/components/video_attachment/video_attachment.vue
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<template>
|
||||
<video class="video"
|
||||
@loadeddata="onVideoDataLoad"
|
||||
:src="attachment.url"
|
||||
:loop="loopVideo"
|
||||
:controls="controls"
|
||||
playsinline
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script src="./video_attachment.js"></script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue