Merge branch 'develop' into feature/improve-status-header-and-spacing

This commit is contained in:
shpuld 2019-02-06 17:44:27 +02:00
commit 4d00475e98
95 changed files with 2271 additions and 430 deletions

View File

@ -95,7 +95,8 @@ module.exports = {
}, },
plugins: [ plugins: [
new ServiceWorkerWebpackPlugin({ new ServiceWorkerWebpackPlugin({
entry: path.join(__dirname, '..', 'src/sw.js') entry: path.join(__dirname, '..', 'src/sw.js'),
filename: 'sw-pleroma.js'
}) })
] ]
} }

View File

@ -6,6 +6,7 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
import FeaturesPanel from './components/features_panel/features_panel.vue' import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import ChatPanel from './components/chat_panel/chat_panel.vue' import ChatPanel from './components/chat_panel/chat_panel.vue'
import MediaModal from './components/media_modal/media_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue' import SideDrawer from './components/side_drawer/side_drawer.vue'
import { unseenNotificationsFromStore } from './services/notification_utils/notification_utils' import { unseenNotificationsFromStore } from './services/notification_utils/notification_utils'
@ -20,6 +21,7 @@ export default {
FeaturesPanel, FeaturesPanel,
WhoToFollowPanel, WhoToFollowPanel,
ChatPanel, ChatPanel,
MediaModal,
SideDrawer SideDrawer
}, },
data: () => ({ data: () => ({
@ -79,7 +81,8 @@ export default {
}, },
unseenNotificationsCount () { unseenNotificationsCount () {
return this.unseenNotifications.length return this.unseenNotifications.length
} },
showFeaturesPanel () { return this.$store.state.config.showFeaturesPanel }
}, },
methods: { methods: {
scrollToTop () { scrollToTop () {

View File

@ -425,6 +425,12 @@ main-router {
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
.faint {
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
a { a {
color: $fallback--link; color: $fallback--link;
color: var(--panelLink, $fallback--link) color: var(--panelLink, $fallback--link)
@ -499,7 +505,7 @@ nav {
} }
.main { .main {
flex-basis: 60%; flex-basis: 50%;
flex-grow: 1; flex-grow: 1;
flex-shrink: 1; flex-shrink: 1;
} }
@ -533,7 +539,7 @@ nav {
} }
} }
@media all and (min-width: 960px) { @media all and (min-width: 800px) {
body { body {
overflow-y: scroll; overflow-y: scroll;
} }
@ -617,7 +623,7 @@ nav {
color: $fallback--faint; color: $fallback--faint;
color: var(--faint, $fallback--faint); color: var(--faint, $fallback--faint);
} }
@media all and (min-width: 959px) { @media all and (min-width: 800px) {
.logo { .logo {
opacity: 1 !important; opacity: 1 !important;
} }
@ -654,7 +660,34 @@ nav {
border-radius: var(--inputRadius, $fallback--inputRadius); border-radius: var(--inputRadius, $fallback--inputRadius);
} }
@media all and (max-width: 959px) { @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);
}
}
@media all and (max-width: 800px) {
.mobile-hidden { .mobile-hidden {
display: none; display: none;
} }

View File

@ -29,7 +29,7 @@
<user-panel></user-panel> <user-panel></user-panel>
<nav-panel></nav-panel> <nav-panel></nav-panel>
<instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel> <instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
<features-panel v-if="!currentUser"></features-panel> <features-panel v-if="!currentUser && showFeaturesPanel"></features-panel>
<who-to-follow-panel v-if="currentUser && suggestionsEnabled"></who-to-follow-panel> <who-to-follow-panel v-if="currentUser && suggestionsEnabled"></who-to-follow-panel>
<notifications v-if="currentUser"></notifications> <notifications v-if="currentUser"></notifications>
</div> </div>
@ -41,6 +41,7 @@
<router-view></router-view> <router-view></router-view>
</transition> </transition>
</div> </div>
<media-modal></media-modal>
</div> </div>
<chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel> <chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel>
</div> </div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -89,6 +89,8 @@ const afterStoreSetup = ({ store, i18n }) => {
if ((config.chatDisabled)) { if ((config.chatDisabled)) {
store.dispatch('disableChat') store.dispatch('disableChat')
} else {
store.dispatch('initializeSocket')
} }
const router = new VueRouter({ const router = new VueRouter({

View File

@ -39,7 +39,7 @@ export default (store) => {
{ name: 'dms', path: '/users/:username/dms', component: DMs }, { name: 'dms', path: '/users/:username/dms', component: DMs },
{ name: 'settings', path: '/settings', component: Settings }, { name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration }, { name: 'registration', path: '/registration', component: Registration },
{ name: 'registration', path: '/registration/:token', component: Registration }, { name: 'registration-token', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
{ name: 'user-settings', path: '/user-settings', component: UserSettings }, { name: 'user-settings', path: '/user-settings', component: UserSettings },
{ name: 'notifications', path: '/:username/notifications', component: Notifications }, { name: 'notifications', path: '/:username/notifications', component: Notifications },

View File

@ -7,6 +7,9 @@ const About = {
InstanceSpecificPanel, InstanceSpecificPanel,
FeaturesPanel, FeaturesPanel,
TermsOfServicePanel TermsOfServicePanel
},
computed: {
showFeaturesPanel () { return this.$store.state.config.showFeaturesPanel }
} }
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="sidebar"> <div class="sidebar">
<instance-specific-panel></instance-specific-panel> <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> <terms-of-service-panel></terms-of-service-panel>
</div> </div>
</template> </template>

View File

@ -1,4 +1,5 @@
import StillImage from '../still-image/still-image.vue' import StillImage from '../still-image/still-image.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue'
import nsfwImage from '../../assets/nsfw.png' import nsfwImage from '../../assets/nsfw.png'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
@ -7,23 +8,29 @@ const Attachment = {
'attachment', 'attachment',
'nsfw', 'nsfw',
'statusId', 'statusId',
'size' 'size',
'allowPlay',
'setMedia'
], ],
data () { data () {
return { return {
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage, nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
hideNsfwLocal: this.$store.state.config.hideNsfw, hideNsfwLocal: this.$store.state.config.hideNsfw,
preloadImage: this.$store.state.config.preloadImage, preloadImage: this.$store.state.config.preloadImage,
loopVideo: this.$store.state.config.loopVideo,
showHidden: false,
loading: 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: { components: {
StillImage StillImage,
VideoAttachment
}, },
computed: { computed: {
usePlaceHolder () {
return this.size === 'hide' || this.type === 'unknown'
},
referrerpolicy () { referrerpolicy () {
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer' return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
}, },
@ -40,7 +47,7 @@ const Attachment = {
return this.size === 'small' return this.size === 'small'
}, },
fullwidth () { fullwidth () {
return fileTypeService.fileType(this.attachment.mimetype) === 'html' return this.type === 'html' || this.type === 'audio'
} }
}, },
methods: { methods: {
@ -49,7 +56,24 @@ const Attachment = {
window.open(target.href, '_blank') 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 && !this.preloadImage) {
if (this.img.onload) { if (this.img.onload) {
this.img.onload() this.img.onload()
@ -64,23 +88,6 @@ const Attachment = {
} else { } else {
this.showHidden = !this.showHidden 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
}
}
} }
} }
} }

View File

@ -1,19 +1,44 @@
<template> <template>
<div v-if="size==='hide'"> <div v-if="usePlaceHolder" @click="openModal">
<a class="placeholder" v-if="type !== 'html'" target="_blank" :href="attachment.url">[{{nsfw ? "NSFW/" : ""}}{{type.toUpperCase()}}]</a> <a class="placeholder"
v-if="type !== 'html'"
target="_blank" :href="attachment.url"
>
[{{nsfw ? "NSFW/" : ""}}{{type.toUpperCase()}}]
</a>
</div> </div>
<div v-else class="attachment" :class="{[type]: true, loading, 'small-attachment': isSmall, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}" v-show="!isEmpty"> <div
<a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()"> v-else class="attachment"
<img :key="nsfwImage" :src="nsfwImage"/> :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> </a>
<div class="hider" v-if="nsfw && hideNsfwLocal && !hidden"> <div class="hider" v-if="nsfw && hideNsfwLocal && !hidden">
<a href="#" @click.prevent="toggleHidden()">Hide</a> <a href="#" @click.prevent="toggleHidden">Hide</a>
</div> </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> </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> <audio v-if="type === 'audio'" :src="attachment.url" controls></audio>
@ -40,12 +65,17 @@
.attachment.media-upload-container { .attachment.media-upload-container {
flex: 0 0 auto; flex: 0 0 auto;
max-height: 300px; max-height: 200px;
max-width: 100%; max-width: 100%;
display: flex;
video {
max-width: 100%;
}
} }
.placeholder { .placeholder {
margin-right: 0.5em; margin-right: 8px;
margin-bottom: 4px;
} }
.nsfw-placeholder { .nsfw-placeholder {
@ -56,17 +86,9 @@
} }
} }
.small-attachment {
&.image, &.video {
max-width: 35%;
}
max-height: 100px;
}
.attachment { .attachment {
position: relative; position: relative;
flex: 1 0 30%; margin: 0.5em 0.5em 0em 0em;
margin: 0.5em 0.7em 0.6em 0.0em;
align-self: flex-start; align-self: flex-start;
line-height: 0; line-height: 0;
@ -78,6 +100,28 @@
border-color: var(--border, $fallback--border); border-color: var(--border, $fallback--border);
overflow: hidden; 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 { .fullwidth {
flex-basis: 100%; flex-basis: 100%;
} }
@ -86,6 +130,28 @@
line-height: 0; 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 { &.html {
flex-basis: 90%; flex-basis: 90%;
width: 100%; width: 100%;
@ -94,6 +160,7 @@
.hider { .hider {
position: absolute; position: absolute;
white-space: nowrap;
margin: 10px; margin: 10px;
padding: 5px; padding: 5px;
background: rgba(230,230,230,0.6); background: rgba(230,230,230,0.6);
@ -104,13 +171,7 @@
border-radius: var(--tooltipRadius, $fallback--tooltipRadius); border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
} }
.small {
max-height: 100px;
}
video { video {
max-height: 500px;
height: 100%;
width: 100%;
z-index: 0; z-index: 0;
} }
@ -120,7 +181,7 @@
img.media-upload { img.media-upload {
line-height: 0; line-height: 0;
max-height: 300px; max-height: 200px;
max-width: 100%; max-width: 100%;
} }
@ -157,29 +218,20 @@
} }
.image-attachment { .image-attachment {
display: flex; width: 100%;
flex: 1; height: 100%;
&.hidden { &.hidden {
display: none; display: none;
} }
.still-image { .nsfw {
object-fit: cover;
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
.small {
img { 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; image-orientation: from-image;
} }
} }

View 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

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

View 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

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

View 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

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

View File

@ -22,19 +22,29 @@ const LoginForm = {
oauth: this.$store.state.oauth, oauth: this.$store.state.oauth,
instance: this.$store.state.instance.server instance: this.$store.state.instance.server
} }
this.clearError()
oauthApi.getOrCreateApp(data).then((app) => { oauthApi.getOrCreateApp(data).then((app) => {
oauthApi.getTokenWithCredentials( oauthApi.getTokenWithCredentials(
{ {
app, app,
instance: data.instance, instance: data.instance,
username: this.user.username, username: this.user.username,
password: this.user.password}) password: this.user.password
.then((result) => { }
).then((result) => {
if (result.error) {
this.authError = result.error
this.user.password = ''
return
}
this.$store.commit('setToken', result.access_token) this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token) this.$store.dispatch('loginUser', result.access_token)
this.$router.push({name: 'friends'}) this.$router.push({name: 'friends'})
}) })
}) })
},
clearError () {
this.authError = false
} }
} }
} }

View File

@ -33,6 +33,13 @@
</div> </div>
</div> </div>
</form> </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>
</div> </div>
</template> </template>
@ -48,10 +55,6 @@
width: 10em; width: 10em;
} }
.error {
text-align: center;
}
.register { .register {
flex: 1 1; flex: 1 1;
} }
@ -64,4 +67,14 @@
justify-content: space-between; justify-content: space-between;
} }
} }
.login {
.error {
text-align: center;
animation-name: shakeError;
animation-duration: 0.4s;
animation-timing-function: ease-in-out;
}
}
</style> </style>

View 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

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

View File

@ -3,19 +3,10 @@ import statusPosterService from '../../services/status_poster/status_poster.serv
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js' import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
const mediaUpload = { 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 () { data () {
return { return {
uploading: false uploading: false,
uploadReady: true
} }
}, },
methods: { methods: {
@ -56,6 +47,18 @@ const mediaUpload = {
} else { } else {
e.dataTransfer.dropEffect = 'none' 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: [ props: [

View File

@ -3,7 +3,7 @@
<label class="btn btn-default" :title="$t('tool_tip.media_upload')"> <label class="btn btn-default" :title="$t('tool_tip.media_upload')">
<i class="icon-spin4 animate-spin" v-if="uploading"></i> <i class="icon-spin4 animate-spin" v-if="uploading"></i>
<i class="icon-upload" 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> </label>
</div> </div>
</template> </template>

View File

@ -44,6 +44,7 @@
.nav-panel .panel { .nav-panel .panel {
overflow: hidden; overflow: hidden;
box-shadow: var(--panelShadow);
} }
.nav-panel ul { .nav-panel ul {
list-style: none; list-style: none;

View File

@ -1,5 +1,5 @@
import Status from '../status/status.vue' 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 UserCardContent from '../user_card_content/user_card_content.vue'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -13,7 +13,7 @@ const Notification = {
}, },
props: [ 'notification' ], props: [ 'notification' ],
components: { components: {
Status, StillImage, UserCardContent Status, UserAvatar, UserCardContent
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {

View File

@ -2,7 +2,7 @@
<status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status> <status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else> <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"> <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> </a>
<div class='notification-right'> <div class='notification-right'>
<div class="usercard notification-usercard" v-if="userExpanded"> <div class="usercard notification-usercard" v-if="userExpanded">

View File

@ -13,6 +13,11 @@ const Notifications = {
notificationsFetcher.startFetching({ store, credentials }) notificationsFetcher.startFetching({ store, credentials })
}, },
data () {
return {
bottomedOut: false
}
},
computed: { computed: {
notifications () { notifications () {
return notificationsFromStore(this.$store) return notificationsFromStore(this.$store)
@ -28,6 +33,9 @@ const Notifications = {
}, },
unseenCount () { unseenCount () {
return this.unseenNotifications.length return this.unseenNotifications.length
},
loading () {
return this.$store.state.statuses.notifications.loading
} }
}, },
components: { components: {
@ -49,10 +57,16 @@ const Notifications = {
fetchOlderNotifications () { fetchOlderNotifications () {
const store = this.$store const store = this.$store
const credentials = store.state.users.currentUser.credentials const credentials = store.state.users.currentUser.credentials
store.commit('setNotificationsLoading', { value: true })
notificationsFetcher.fetchAndUpdate({ notificationsFetcher.fetchAndUpdate({
store, store,
credentials, credentials,
older: true older: true
}).then(notifs => {
store.commit('setNotificationsLoading', { value: false })
if (notifs.length === 0) {
this.bottomedOut = true
}
}) })
} }
} }

View File

@ -36,26 +36,7 @@
border-color: $fallback--border; border-color: $fallback--border;
border-color: var(--border, $fallback--border); border-color: var(--border, $fallback--border);
.avatar-compact { &:hover .animated.avatar {
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 {
canvas { canvas {
display: none; display: none;
} }

View File

@ -18,10 +18,15 @@
</div> </div>
</div> </div>
<div class="panel-footer"> <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> <div class="new-status-notification text-center panel-footer">{{$t('notifications.load_older')}}</div>
</a> </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> </div>
</div> </div>

View File

@ -16,7 +16,7 @@ const buildMentionsString = ({user, attentions}, currentUser) => {
return `@${attention.screen_name}` return `@${attention.screen_name}`
}) })
return mentions.join(' ') + ' ' return mentions.length > 0 ? mentions.join(' ') + ' ' : ''
} }
const PostStatusForm = { const PostStatusForm = {
@ -250,6 +250,7 @@ const PostStatusForm = {
visibility: newStatus.visibility, visibility: newStatus.visibility,
contentType: newStatus.contentType contentType: newStatus.contentType
} }
this.$refs.mediaUpload.clearFile()
this.$emit('posted') this.$emit('posted')
let el = this.$el.querySelector('textarea') let el = this.$el.querySelector('textarea')
el.style.height = 'auto' el.style.height = 'auto'

View File

@ -64,7 +64,7 @@
</div> </div>
</div> </div>
<div class='form-bottom'> <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 v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p>
<p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p> <p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p>

View File

@ -147,24 +147,6 @@ $validations-cRed: #f04124;
margin-bottom: 1em; 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 { .form-group--error {
animation-name: shakeError; animation-name: shakeError;
animation-duration: .6s; 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 { .registration-form .container {
flex-direction: column-reverse; flex-direction: column-reverse;
} }

View File

@ -1,5 +1,5 @@
/* eslint-env browser */ /* 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 StyleSwitcher from '../style_switcher/style_switcher.vue'
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue' import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
import { filter, trim } from 'lodash' import { filter, trim } from 'lodash'
@ -13,6 +13,7 @@ const settings = {
hideAttachmentsLocal: user.hideAttachments, hideAttachmentsLocal: user.hideAttachments,
hideAttachmentsInConvLocal: user.hideAttachmentsInConv, hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
hideNsfwLocal: user.hideNsfw, hideNsfwLocal: user.hideNsfw,
useOneClickNsfw: user.useOneClickNsfw,
hideISPLocal: user.hideISP, hideISPLocal: user.hideISP,
preloadImage: user.preloadImage, preloadImage: user.preloadImage,
@ -29,7 +30,6 @@ const settings = {
notificationVisibilityLocal: user.notificationVisibility, notificationVisibilityLocal: user.notificationVisibility,
replyVisibilityLocal: user.replyVisibility, replyVisibilityLocal: user.replyVisibility,
loopVideoLocal: user.loopVideo, loopVideoLocal: user.loopVideo,
loopVideoSilentOnlyLocal: user.loopVideoSilentOnly,
muteWordsString: user.muteWords.join('\n'), muteWordsString: user.muteWords.join('\n'),
autoLoadLocal: user.autoLoad, autoLoadLocal: user.autoLoad,
streamingLocal: user.streaming, streamingLocal: user.streaming,
@ -58,13 +58,16 @@ const settings = {
stopGifs: user.stopGifs, stopGifs: user.stopGifs,
webPushNotificationsLocal: user.webPushNotifications, webPushNotificationsLocal: user.webPushNotifications,
loopVideoSilentOnlyLocal: user.loopVideosSilentOnly,
loopSilentAvailable: loopSilentAvailable:
// Firefox // Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
// Chrome-likes // Chrome-likes
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') || Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
// Future spec, still not supported in Nightly 63 as of 08/2018 // 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: { components: {
@ -96,6 +99,9 @@ const settings = {
hideNsfwLocal (value) { hideNsfwLocal (value) {
this.$store.dispatch('setOption', { name: 'hideNsfw', value }) this.$store.dispatch('setOption', { name: 'hideNsfw', value })
}, },
useOneClickNsfw (value) {
this.$store.dispatch('setOption', { name: 'useOneClickNsfw', value })
},
preloadImage (value) { preloadImage (value) {
this.$store.dispatch('setOption', { name: 'preloadImage', value }) this.$store.dispatch('setOption', { name: 'preloadImage', value })
}, },
@ -157,6 +163,12 @@ const settings = {
webPushNotificationsLocal (value) { webPushNotificationsLocal (value) {
this.$store.dispatch('setOption', { name: 'webPushNotifications', value }) this.$store.dispatch('setOption', { name: 'webPushNotifications', value })
if (value) this.$store.dispatch('registerPushNotifications') if (value) this.$store.dispatch('registerPushNotifications')
},
playVideosInModal (value) {
this.$store.dispatch('setOption', { name: 'playVideosInModal', value })
},
useContainFit (value) {
this.$store.dispatch('setOption', { name: 'useContainFit', value })
} }
} }
} }

View File

@ -123,6 +123,10 @@
<input :disabled="!hideNsfwLocal" type="checkbox" id="preloadImage" v-model="preloadImage"> <input :disabled="!hideNsfwLocal" type="checkbox" id="preloadImage" v-model="preloadImage">
<label for="preloadImage">{{$t('settings.preload_images')}}</label> <label for="preloadImage">{{$t('settings.preload_images')}}</label>
</li> </li>
<li>
<input type="checkbox" id="useOneClickNsfw" v-model="useOneClickNsfw">
<label for="useOneClickNsfw">{{$t('settings.use_one_click_nsfw')}}</label>
</li>
</ul> </ul>
<li> <li>
<input type="checkbox" id="stopGifs" v-model="stopGifs"> <input type="checkbox" id="stopGifs" v-model="stopGifs">
@ -141,6 +145,14 @@
</li> </li>
</ul> </ul>
</li> </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> </ul>
</div> </div>

View File

@ -26,6 +26,12 @@ const SideDrawer = {
}, },
suggestionsEnabled () { suggestionsEnabled () {
return this.$store.state.instance.suggestionsEnabled return this.$store.state.instance.suggestionsEnabled
},
logo () {
return this.$store.state.instance.logo
},
sitename () {
return this.$store.state.instance.name
} }
}, },
methods: { methods: {

View File

@ -8,8 +8,11 @@
@touchmove="touchMove" @touchmove="touchMove"
> >
<div class="side-drawer-heading" @click="toggleDrawer"> <div class="side-drawer-heading" @click="toggleDrawer">
<user-card-content :user="currentUser" :switcher="false" :hideBio="true" v-if="currentUser"> <user-card-content :user="currentUser" :switcher="false" :hideBio="true" v-if="currentUser"/>
</user-card-content> <div class="side-drawer-logo-wrapper" v-else>
<img :src="logo"/>
<span>{{sitename}}</span>
</div>
</div> </div>
<ul> <ul>
<li v-if="currentUser" @click="toggleDrawer"> <li v-if="currentUser" @click="toggleDrawer">
@ -141,6 +144,24 @@
background-color: var(--bg, $fallback--bg); 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 { .side-drawer-click-outside-closed {
flex: 0 0 0; flex: 0 0 0;
} }
@ -154,7 +175,6 @@
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
display: flex; display: flex;
min-height: 7em;
padding: 0; padding: 0;
margin: 0; margin: 0;

View File

@ -4,10 +4,14 @@ import RetweetButton from '../retweet_button/retweet_button.vue'
import DeleteButton from '../delete_button/delete_button.vue' import DeleteButton from '../delete_button/delete_button.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' 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 { filter, find } from 'lodash' import Gallery from '../gallery/gallery.vue'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import LinkPreview from '../link-preview/link-preview.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' 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 = { const Status = {
name: 'Status', name: 'Status',
@ -31,11 +35,13 @@ const Status = {
userExpanded: false, userExpanded: false,
preview: null, preview: null,
showPreview: false, showPreview: false,
showingTall: false, showingTall: this.inConversation && this.focused,
showingLongSubject: false,
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined' expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
? !this.$store.state.instance.collapseMessageWithSubject ? !this.$store.state.instance.collapseMessageWithSubject
: !this.$store.state.config.collapseMessageWithSubject, : !this.$store.state.config.collapseMessageWithSubject,
betterShadow: this.$store.state.interface.browserSupport.cssFilter betterShadow: this.$store.state.interface.browserSupport.cssFilter,
maxAttachments: 9
} }
}, },
computed: { computed: {
@ -78,12 +84,13 @@ const Status = {
}, },
replyProfileLink () { replyProfileLink () {
if (this.isReply) { 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 }, 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 }, retweeterHtml () { return this.statusoid.user.name_html },
retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },
status () { status () {
if (this.retweet) { if (this.retweet) {
return this.statusoid.retweeted_status 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 const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
return lengthScore > 20 return lengthScore > 20
}, },
longSubject () {
return this.status.summary.length > 900
},
isReply () { 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 () { replyToName () {
const user = this.$store.state.users.usersObject[this.status.in_reply_to_user_id] const user = this.$store.state.users.usersObject[this.status.in_reply_to_user_id]
@ -178,7 +188,7 @@ const Status = {
return this.tallStatus return this.tallStatus
}, },
showingMore () { showingMore () {
return this.showingTall || (this.status.summary && this.expandingSubject) return (this.tallStatus && this.showingTall) || (this.status.summary && this.expandingSubject)
}, },
nsfwClickthrough () { nsfwClickthrough () {
if (!this.status.nsfw) { if (!this.status.nsfw) {
@ -205,12 +215,31 @@ const Status = {
}, },
attachmentSize () { attachmentSize () {
if ((this.$store.state.config.hideAttachments && !this.inConversation) || 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' return 'hide'
} else if (this.compact) { } else if (this.compact) {
return 'small' return 'small'
} }
return 'normal' 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: { components: {
@ -220,7 +249,9 @@ const Status = {
DeleteButton, DeleteButton,
PostStatusForm, PostStatusForm,
UserCardContent, UserCardContent,
StillImage UserAvatar,
Gallery,
LinkPreview
}, },
methods: { methods: {
visibilityIcon (visibility) { visibilityIcon (visibility) {
@ -235,11 +266,23 @@ const Status = {
return 'icon-globe' return 'icon-globe'
} }
}, },
linkClicked ({target}) { linkClicked (event) {
let { target } = event
if (target.tagName === 'SPAN') { if (target.tagName === 'SPAN') {
target = target.parentNode target = target.parentNode
} }
if (target.tagName === 'A') { 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') window.open(target.href, '_blank')
} }
}, },
@ -264,11 +307,11 @@ const Status = {
toggleShowMore () { toggleShowMore () {
if (this.showingTall) { if (this.showingTall) {
this.showingTall = false this.showingTall = false
} else if (this.expandingSubject) { } else if (this.expandingSubject && this.status.summary) {
this.expandingSubject = false this.expandingSubject = false
} else if (this.hideTallStatus) { } else if (this.hideTallStatus) {
this.showingTall = true this.showingTall = true
} else if (this.hideSubjectStatus) { } else if (this.hideSubjectStatus && this.status.summary) {
this.expandingSubject = true this.expandingSubject = true
} }
}, },
@ -295,6 +338,10 @@ const Status = {
}, },
generateUserProfileLink (id, name) { generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) 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: { watch: {
@ -302,8 +349,13 @@ const Status = {
if (this.status.id === id) { if (this.status.id === id) {
let rect = this.$el.getBoundingClientRect() let rect = this.$el.getBoundingClientRect()
if (rect.top < 100) { 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) { } 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) window.scrollBy(0, rect.bottom - window.innerHeight + 50)
} }
} }

View File

@ -13,10 +13,13 @@
</template> </template>
<template v-else> <template v-else>
<div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info"> <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"> <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> <span class="user-name">
<a v-else :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a> <router-link :to="retweeterProfileLink">
{{retweeterHtml || retweeter}}
</router-link>
</span>
<i class='fa icon-retweet retweeted' :title="$t('tool_tip.repeat')"></i> <i class='fa icon-retweet retweeted' :title="$t('tool_tip.repeat')"></i>
{{$t('timeline.repeated')}} {{$t('timeline.repeated')}}
</div> </div>
@ -24,9 +27,9 @@
<div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet }]" :style="[ userStyle ]" class="media status"> <div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet }]" :style="[ userStyle ]" class="media status">
<div v-if="!noHeading" class="media-left"> <div v-if="!noHeading" class="media-left">
<a :href="status.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded"> <router-link :to="userProfileLink" @click.stop.prevent.capture.native="toggleUserExpanded">
<StillImage class='avatar' :class="{'avatar-compact': compact, 'better-shadow': betterShadow}" :src="status.user.profile_image_url_original"/> <UserAvatar :compact="compact" :betterShadow="betterShadow" :src="status.user.profile_image_url_original"/>
</a> </router-link>
</div> </div>
<div class="status-body"> <div class="status-body">
<div class="usercard media-body" v-if="userExpanded"> <div class="usercard media-body" v-if="userExpanded">
@ -86,7 +89,12 @@
</div> </div>
</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> <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.statusnet_html" v-if="!hideSubjectStatus"></div>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary_html" v-else></div> <div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary_html" v-else></div>
@ -94,9 +102,27 @@
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">Show less</a> <a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">Show less</a>
</div> </div>
<div v-if='status.attachments && !hideSubjectStatus' class='attachments media-body'> <div v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)" 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
</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>
<div v-if="!noHeading && !noReplyLinks" class='status-actions media-body'> <div v-if="!noHeading && !noReplyLinks" class='status-actions media-body'>
@ -239,6 +265,11 @@
vertical-align: bottom; vertical-align: bottom;
flex-basis: 100%; flex-basis: 100%;
a {
display: inline-block;
word-break: break-all;
}
small { small {
font-weight: lighter; font-weight: lighter;
} }
@ -272,6 +303,14 @@
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
} }
& > span {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
& > a:last-child {
flex-shrink: 0;
}
} }
.reply-to-text { .reply-to-text {
margin-right: 0.5em; margin-right: 0.5em;
@ -309,11 +348,6 @@
} }
} }
a {
display: inline-block;
word-break: break-all;
}
.tall-status { .tall-status {
position: relative; position: relative;
height: 220px; height: 220px;
@ -322,6 +356,8 @@
} }
.tall-status-hider { .tall-status-hider {
display: inline-block;
word-break: break-all;
position: absolute; position: absolute;
height: 70px; height: 70px;
margin-top: 150px; margin-top: 150px;
@ -339,6 +375,8 @@
.status-unhider, .cw-status-hider { .status-unhider, .cw-status-hider {
width: 100%; width: 100%;
text-align: center; text-align: center;
display: inline-block;
word-break: break-all;
} }
.status-content { .status-content {
@ -396,7 +434,7 @@
padding: 0.4em 0.6em 0 0.6em; padding: 0.4em 0.6em 0 0.6em;
margin: 0; margin: 0;
.avatar { .avatar.still-image {
border-radius: $fallback--avatarAltRadius; border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
margin-left: 28px; margin-left: 28px;
@ -469,46 +507,6 @@
color: var(--cBlue, $fallback--cBlue); 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 { .status:hover .animated.avatar {
canvas { canvas {
display: none; display: none;
@ -563,10 +561,10 @@ a.unmute {
} }
} }
@media all and (max-width: 960px) { @media all and (max-width: 800px) {
.status-el { .status-el {
.retweet-info { .retweet-info {
.avatar { .avatar.still-image {
margin-left: 20px; margin-left: 20px;
} }
} }
@ -575,15 +573,15 @@ a.unmute {
max-width: 100%; max-width: 100%;
} }
.status .avatar { .status .avatar.still-image {
width: 40px; width: 40px;
height: 40px; height: 40px;
}
.status .avatar-compact { &.avatar-compact {
width: 32px; width: 32px;
height: 32px; height: 32px;
} }
} }
}
</style> </style>

View File

@ -2,7 +2,8 @@ const StillImage = {
props: [ props: [
'src', 'src',
'referrerpolicy', 'referrerpolicy',
'mimetype' 'mimetype',
'imageLoadError'
], ],
data () { data () {
return { return {
@ -23,6 +24,9 @@ const StillImage = {
canvas.width = width canvas.width = width
canvas.height = height canvas.height = height
canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height) canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height)
},
onError () {
this.imageLoadError && this.imageLoadError()
} }
} }
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class='still-image' :class='{ animated: animated }' > <div class='still-image' :class='{ animated: animated }' >
<canvas ref="canvas" v-if="animated"></canvas> <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> </div>
</template> </template>

View File

@ -7,7 +7,7 @@ import OpacityInput from '../opacity_input/opacity_input.vue'
import ShadowControl from '../shadow_control/shadow_control.vue' import ShadowControl from '../shadow_control/shadow_control.vue'
import FontControl from '../font_control/font_control.vue' import FontControl from '../font_control/font_control.vue'
import ContrastRatio from '../contrast_ratio/contrast_ratio.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 Preview from './preview.vue'
import ExportImport from '../export_import/export_import.vue' import ExportImport from '../export_import/export_import.vue'

View File

@ -4,6 +4,7 @@ import './tab_switcher.scss'
export default Vue.component('tab-switcher', { export default Vue.component('tab-switcher', {
name: 'TabSwitcher', name: 'TabSwitcher',
props: ['renderOnlyFocused'],
data () { data () {
return { return {
active: this.$slots.default.findIndex(_ => _.tag) active: this.$slots.default.findIndex(_ => _.tag)
@ -44,11 +45,12 @@ export default Vue.component('tab-switcher', {
const contents = this.$slots.default.map((slot, index) => { const contents = this.$slots.default.map((slot, index) => {
if (!slot.tag) return if (!slot.tag) return
const active = index === this.active const active = index === this.active
return ( if (this.renderOnlyFocused) {
<div class={active ? 'active' : 'hidden'}> return active
{slot} ? <div class="active">{slot}</div>
</div> : <div class="hidden"></div>
) }
return <div class={active ? 'active' : 'hidden' }>{slot}</div>
}) })
return ( return (

View File

@ -16,7 +16,8 @@ const Timeline = {
data () { data () {
return { return {
paused: false, paused: false,
unfocused: false unfocused: false,
bottomedOut: false
} }
}, },
computed: { computed: {
@ -95,7 +96,12 @@ const Timeline = {
showImmediately: true, showImmediately: true,
userId: this.userId, userId: this.userId,
tag: this.tag 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), }, 1000, this),
scrollLoad (e) { scrollLoad (e) {
const bodyBRect = document.body.getBoundingClientRect() const bodyBRect = document.body.getBoundingClientRect()

View File

@ -20,10 +20,15 @@
</div> </div>
</div> </div>
<div :class="classes.footer"> <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> <div class="new-status-notification text-center panel-footer">{{$t('timeline.load_older')}}</div>
</a> </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> </div>
</template> </template>

View 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

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

View File

@ -1,5 +1,5 @@
import UserCardContent from '../user_card_content/user_card_content.vue' 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' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const UserCard = { const UserCard = {
@ -15,7 +15,7 @@ const UserCard = {
}, },
components: { components: {
UserCardContent, UserCardContent,
StillImage UserAvatar
}, },
computed: { computed: {
currentUser () { return this.$store.state.users.currentUser } currentUser () { return this.$store.state.users.currentUser }

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="card"> <div class="card">
<a href="#"> <router-link :to="userProfileLink(user)">
<StillImage @click.prevent="toggleUserExpanded" class="avatar" :src="user.profile_image_url"/> <UserAvatar class="avatar" :compact="true" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
</a> </router-link>
<div class="usercard" v-if="userExpanded"> <div class="usercard" v-if="userExpanded">
<user-card-content :user="user" :switcher="false"></user-card-content> <user-card-content :user="user" :switcher="false"></user-card-content>
</div> </div>
@ -69,17 +69,13 @@
border-bottom-color: var(--border, $fallback--border); border-bottom-color: var(--border, $fallback--border);
.avatar { .avatar {
margin-top: 0.2em; padding: 0;
width:32px;
height: 32px;
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
} }
} }
.usercard { .usercard {
width: fill-available; width: fill-available;
margin: 0.2em 0 0.7em 0; margin: 0.2em 0 0 0.7em;
border-radius: $fallback--panelRadius; border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius); border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid; border-style: solid;

View File

@ -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 { hex2rgb } from '../../services/color_convert/color_convert.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -82,7 +82,7 @@ export default {
} }
}, },
components: { components: {
StillImage UserAvatar
}, },
methods: { methods: {
followUser () { followUser () {

View File

@ -4,7 +4,7 @@
<div class='user-info'> <div class='user-info'>
<div class='container'> <div class='container'>
<router-link :to="userProfileLink(user)"> <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> </router-link>
<div class="name-and-screen-name"> <div class="name-and-screen-name">
<div class="top-line"> <div class="top-line">
@ -107,18 +107,18 @@
</div> </div>
</div> </div>
<div class="panel-body profile-panel-body" v-if="!hideBio"> <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')"> <div class="user-count" v-on:click.prevent="setProfileView('statuses')">
<h5>{{ $t('user_card.statuses') }}</h5> <h5>{{ $t('user_card.statuses') }}</h5>
<span v-if="!hideUserStatsLocal">{{user.statuses_count}} <br></span> <span>{{user.statuses_count}} <br></span>
</div> </div>
<div class="user-count" v-on:click.prevent="setProfileView('friends')"> <div class="user-count" v-on:click.prevent="setProfileView('friends')">
<h5>{{ $t('user_card.followees') }}</h5> <h5>{{ $t('user_card.followees') }}</h5>
<span v-if="!hideUserStatsLocal">{{user.friends_count}}</span> <span>{{user.friends_count}}</span>
</div> </div>
<div class="user-count" v-on:click.prevent="setProfileView('followers')"> <div class="user-count" v-on:click.prevent="setProfileView('followers')">
<h5>{{ $t('user_card.followers') }}</h5> <h5>{{ $t('user_card.followers') }}</h5>
<span v-if="!hideUserStatsLocal">{{user.followers_count}}</span> <span>{{user.followers_count}}</span>
</div> </div>
</div> </div>
<p @click.prevent="linkClicked" v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p> <p @click.prevent="linkClicked" v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p>
@ -169,23 +169,12 @@
max-height: 56px; max-height: 56px;
.avatar { .avatar {
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
flex: 1 0 100%; flex: 1 0 100%;
width: 56px; width: 56px;
height: 56px; height: 56px;
box-shadow: 0px 1px 8px rgba(0,0,0,0.75); box-shadow: 0px 1px 8px rgba(0,0,0,0.75);
box-shadow: var(--avatarShadow); box-shadow: var(--avatarShadow);
object-fit: cover; object-fit: cover;
&.better-shadow {
box-shadow: var(--avatarShadowInset);
filter: var(--avatarShadowFilter)
}
&.animated::before {
display: none;
}
} }
} }

View File

@ -1,6 +1,7 @@
import UserCardContent from '../user_card_content/user_card_content.vue' import UserCardContent from '../user_card_content/user_card_content.vue'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import Timeline from '../timeline/timeline.vue' import Timeline from '../timeline/timeline.vue'
import FollowList from '../follow_list/follow_list.vue'
const UserProfile = { const UserProfile = {
created () { created () {
@ -8,16 +9,14 @@ const UserProfile = {
this.$store.commit('clearTimeline', { timeline: 'favorites' }) this.$store.commit('clearTimeline', { timeline: 'favorites' })
this.$store.commit('clearTimeline', { timeline: 'media' }) this.$store.commit('clearTimeline', { timeline: 'media' })
this.$store.dispatch('startFetching', ['user', this.fetchBy]) this.$store.dispatch('startFetching', ['user', this.fetchBy])
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
this.$store.dispatch('startFetching', ['media', this.fetchBy]) this.$store.dispatch('startFetching', ['media', this.fetchBy])
this.startFetchFavorites()
if (!this.user.id) { if (!this.user.id) {
this.$store.dispatch('fetchUser', this.fetchBy) this.$store.dispatch('fetchUser', this.fetchBy)
} }
}, },
destroyed () { destroyed () {
this.$store.dispatch('stopFetching', 'user') this.cleanUp(this.userId)
this.$store.dispatch('stopFetching', 'favorites')
this.$store.dispatch('stopFetching', 'media')
}, },
computed: { computed: {
timeline () { timeline () {
@ -36,13 +35,8 @@ const UserProfile = {
return this.$route.params.name || this.user.screen_name return this.$route.params.name || this.user.screen_name
}, },
isUs () { isUs () {
return this.userId === this.$store.state.users.currentUser.id return this.userId && this.$store.state.users.currentUser.id &&
}, this.userId === this.$store.state.users.currentUser.id
friends () {
return this.user.friends
},
followers () {
return this.user.followers
}, },
userInStore () { userInStore () {
if (this.isExternal) { if (this.isExternal) {
@ -67,56 +61,47 @@ const UserProfile = {
} }
}, },
methods: { methods: {
fetchFollowers () { startFetchFavorites () {
const id = this.userId if (this.isUs) {
this.$store.dispatch('addFollowers', { id }) this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
},
fetchFriends () {
const id = this.userId
this.$store.dispatch('addFriends', { id })
} }
}, },
watch: { startUp () {
// TODO get rid of this copypasta this.$store.dispatch('startFetching', ['user', this.fetchBy])
userName () { this.$store.dispatch('startFetching', ['media', this.fetchBy])
if (this.isExternal) {
return this.startFetchFavorites()
} },
cleanUp () {
this.$store.dispatch('stopFetching', 'user') this.$store.dispatch('stopFetching', 'user')
this.$store.dispatch('stopFetching', 'favorites') this.$store.dispatch('stopFetching', 'favorites')
this.$store.dispatch('stopFetching', 'media') this.$store.dispatch('stopFetching', 'media')
this.$store.commit('clearTimeline', { timeline: 'user' }) this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.commit('clearTimeline', { timeline: 'favorites' }) this.$store.commit('clearTimeline', { timeline: 'favorites' })
this.$store.commit('clearTimeline', { timeline: 'media' }) 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]) watch: {
userName () {
if (this.isExternal) {
return
}
this.cleanUp()
this.startUp()
}, },
userId () { userId () {
if (!this.isExternal) { if (!this.isExternal) {
return return
} }
this.$store.dispatch('stopFetching', 'user') this.cleanUp()
this.$store.dispatch('stopFetching', 'favorites') this.startUp()
this.$store.dispatch('stopFetching', 'media')
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])
},
user () {
if (this.user.id && !this.user.followers) {
this.fetchFollowers()
this.fetchFriends()
}
} }
}, },
components: { components: {
UserCardContent, UserCardContent,
UserCard, UserCard,
Timeline Timeline,
FollowList
} }
} }

View File

@ -1,27 +1,47 @@
<template> <template>
<div> <div>
<div v-if="user.id" class="user-profile panel panel-default"> <div v-if="user.id" class="user-profile panel panel-default">
<user-card-content :user="user" :switcher="true" :selected="timeline.viewing"></user-card-content> <user-card-content
<tab-switcher> :user="user"
<Timeline :label="$t('user_card.statuses')" :embedded="true" :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="fetchBy"/> :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')"> <div :label="$t('user_card.followees')">
<div v-if="friends"> <FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" />
<user-card v-for="friend in friends" :key="friend.id" :user="friend" :showFollows="true"></user-card>
</div>
<div class="userlist-placeholder" v-else> <div class="userlist-placeholder" v-else>
<i class="icon-spin3 animate-spin"></i> <i class="icon-spin3 animate-spin"></i>
</div> </div>
</div> </div>
<div :label="$t('user_card.followers')"> <div :label="$t('user_card.followers')">
<div v-if="followers"> <FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" />
<user-card v-for="follower in followers" :key="follower.id" :user="follower" :showFollows="false"></user-card>
</div>
<div class="userlist-placeholder" v-else> <div class="userlist-placeholder" v-else>
<i class="icon-spin3 animate-spin"></i> <i class="icon-spin3 animate-spin"></i>
</div> </div>
</div> </div>
<Timeline :label="$t('user_card.media')" :embedded="true" :title="$t('user_profile.media_title')" timeline-name="media" :timeline="media" :user-id="fetchBy" /> <Timeline
<Timeline v-if="isUs" :label="$t('user_card.favorites')" :embedded="true" :title="$t('user_profile.favorites_title')" timeline-name="favorites" :timeline="favorites"/> :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> </tab-switcher>
</div> </div>
<div v-else class="panel user-profile-placeholder"> <div v-else class="panel user-profile-placeholder">

View File

@ -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 StyleSwitcher from '../style_switcher/style_switcher.vue'
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js' import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
@ -6,11 +8,12 @@ const UserSettings = {
data () { data () {
return { return {
newName: this.$store.state.users.currentUser.name, 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, newLocked: this.$store.state.users.currentUser.locked,
newNoRichText: this.$store.state.users.currentUser.no_rich_text, newNoRichText: this.$store.state.users.currentUser.no_rich_text,
newDefaultScope: this.$store.state.users.currentUser.default_scope, 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,
followList: null, followList: null,
followImportError: false, followImportError: false,
followsImported: false, followsImported: false,
@ -66,7 +69,8 @@ const UserSettings = {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
const default_scope = this.newDefaultScope const default_scope = this.newDefaultScope
const no_rich_text = this.newNoRichText const no_rich_text = this.newNoRichText
const hide_network = this.newHideNetwork const hide_follows = this.hideFollows
const hide_followers = this.hideFollowers
/* eslint-enable camelcase */ /* eslint-enable camelcase */
this.$store.state.api.backendInteractor this.$store.state.api.backendInteractor
.updateProfile({ .updateProfile({
@ -78,7 +82,8 @@ const UserSettings = {
/* eslint-disable camelcase */ /* eslint-disable camelcase */
default_scope, default_scope,
no_rich_text, no_rich_text,
hide_network hide_follows,
hide_followers
/* eslint-enable camelcase */ /* eslint-enable camelcase */
}}).then((user) => { }}).then((user) => {
if (!user.error) { if (!user.error) {

View File

@ -30,13 +30,18 @@
<label for="account-no-rich-text">{{$t('settings.no_rich_text_description')}}</label> <label for="account-no-rich-text">{{$t('settings.no_rich_text_description')}}</label>
</p> </p>
<p> <p>
<input type="checkbox" v-model="newHideNetwork" id="account-hide-network"> <input type="checkbox" v-model="hideFollows" id="account-hide-follows">
<label for="account-hide-network">{{$t('settings.hide_network_description')}}</label> <label for="account-hide-follows">{{$t('settings.hide_follows_description')}}</label>
</p> </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>
<button :disabled='newName && newName.length === 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
</div> </div>
<div class="setting-item"> <div class="setting-item">
<h2>{{$t('settings.avatar')}}</h2> <h2>{{$t('settings.avatar')}}</h2>
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
<p>{{$t('settings.current_avatar')}}</p> <p>{{$t('settings.current_avatar')}}</p>
<img :src="user.profile_image_url_original" class="old-avatar"></img> <img :src="user.profile_image_url_original" class="old-avatar"></img>
<p>{{$t('settings.set_new_avatar')}}</p> <p>{{$t('settings.set_new_avatar')}}</p>
@ -126,7 +131,7 @@
<div class="setting-item"> <div class="setting-item">
<h2>{{$t('settings.follow_import')}}</h2> <h2>{{$t('settings.follow_import')}}</h2>
<p>{{$t('settings.import_followers_from_a_csv_file')}}</p> <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> <input type="file" ref="followlist" v-on:change="followListChange"></input>
</form> </form>
<i class=" icon-spin4 animate-spin uploading" v-if="followListUploading"></i> <i class=" icon-spin4 animate-spin uploading" v-if="followListUploading"></i>
@ -175,5 +180,9 @@
font-size: 1.5em; font-size: 1.5em;
margin: 0.25em; margin: 0.25em;
} }
.name-changer {
width: 100%;
}
} }
</style> </style>

View 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

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

View File

@ -155,7 +155,8 @@
"notification_visibility_mentions": "Erwähnungen", "notification_visibility_mentions": "Erwähnungen",
"notification_visibility_repeats": "Wiederholungen", "notification_visibility_repeats": "Wiederholungen",
"no_rich_text_description": "Rich-Text Formatierungen von allen Beiträgen entfernen", "no_rich_text_description": "Rich-Text Formatierungen von allen Beiträgen entfernen",
"hide_network_description": "Zeige nicht, wem ich folge und wer mir folgt", "hide_follows_description": "Zeige nicht, wem ich folge",
"hide_followers_description": "Zeige nicht, wer mir folgt",
"nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind", "nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind",
"panelRadius": "Panel", "panelRadius": "Panel",
"pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist", "pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist",

View File

@ -17,7 +17,9 @@
}, },
"general": { "general": {
"apply": "Apply", "apply": "Apply",
"submit": "Submit" "submit": "Submit",
"more": "More",
"generic_error": "An error occured"
}, },
"login": { "login": {
"login": "Log in", "login": "Log in",
@ -49,7 +51,8 @@
"load_older": "Load older notifications", "load_older": "Load older notifications",
"notifications": "Notifications", "notifications": "Notifications",
"read": "Read!", "read": "Read!",
"repeated_you": "repeated your status" "repeated_you": "repeated your status",
"no_more_notifications": "No more notifications"
}, },
"post_status": { "post_status": {
"new_status": "Post new status", "new_status": "Post new status",
@ -117,6 +120,7 @@
"delete_account_description": "Permanently delete your account and all your messages.", "delete_account_description": "Permanently delete your account and all your messages.",
"delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.", "delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
"delete_account_instructions": "Type your password in the input below to confirm account deletion.", "delete_account_instructions": "Type your password in the input below to confirm account deletion.",
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
"export_theme": "Save preset", "export_theme": "Save preset",
"filtering": "Filtering", "filtering": "Filtering",
"filtering_explanation": "All statuses containing these words will be muted, one per line", "filtering_explanation": "All statuses containing these words will be muted, one per line",
@ -132,6 +136,7 @@
"hide_attachments_in_tl": "Hide attachments in timeline", "hide_attachments_in_tl": "Hide attachments in timeline",
"hide_isp": "Hide instance-specific panel", "hide_isp": "Hide instance-specific panel",
"preload_images": "Preload images", "preload_images": "Preload images",
"use_one_click_nsfw": "Open NSFW attachments with just one click",
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)", "hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
"hide_user_stats": "Hide user statistics (e.g. the number of followers)", "hide_user_stats": "Hide user statistics (e.g. the number of followers)",
"import_followers_from_a_csv_file": "Import follows from a csv file", "import_followers_from_a_csv_file": "Import follows from a csv file",
@ -148,6 +153,8 @@
"lock_account_description": "Restrict your account to approved followers only", "lock_account_description": "Restrict your account to approved followers only",
"loop_video": "Loop videos", "loop_video": "Loop videos",
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")", "loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
"play_videos_in_modal": "Play videos directly in the media viewer",
"use_contain_fit": "Don't crop the attachment in thumbnails",
"name": "Name", "name": "Name",
"name_bio": "Name & Bio", "name_bio": "Name & Bio",
"new_password": "New password", "new_password": "New password",
@ -157,7 +164,8 @@
"notification_visibility_mentions": "Mentions", "notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats", "notification_visibility_repeats": "Repeats",
"no_rich_text_description": "Strip rich text formatting from all posts", "no_rich_text_description": "Strip rich text formatting from all posts",
"hide_network_description": "Don't show who I'm following and who's following me", "hide_follows_description": "Don't show who I'm following",
"hide_followers_description": "Don't show who's following me",
"nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding", "nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
"panelRadius": "Panels", "panelRadius": "Panels",
"pause_on_unfocused": "Pause streaming when tab is not focused", "pause_on_unfocused": "Pause streaming when tab is not focused",
@ -317,7 +325,8 @@
"no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated", "no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated",
"repeated": "repeated", "repeated": "repeated",
"show_new": "Show new", "show_new": "Show new",
"up_to_date": "Up-to-date" "up_to_date": "Up-to-date",
"no_more_statuses": "No more statuses"
}, },
"user_card": { "user_card": {
"approve": "Approve", "approve": "Approve",

View File

@ -2,16 +2,28 @@
"chat": { "chat": {
"title": "Chat" "title": "Chat"
}, },
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
"media_proxy": "Media proxy",
"scope_options": "Opciones del alcance de la visibilidad",
"text_limit": "Límite de carácteres",
"title": "Características",
"who_to_follow": "A quién seguir"
},
"finder": { "finder": {
"error_fetching_user": "Error al buscar usuario", "error_fetching_user": "Error al buscar usuario",
"find_user": "Encontrar usuario" "find_user": "Encontrar usuario"
}, },
"general": { "general": {
"apply": "Aplicar", "apply": "Aplicar",
"submit": "Enviar" "submit": "Enviar",
"more": "Más",
"generic_error": "Ha ocurrido un error"
}, },
"login": { "login": {
"login": "Identificación", "login": "Identificación",
"description": "Identificación con OAuth",
"logout": "Salir", "logout": "Salir",
"password": "Contraseña", "password": "Contraseña",
"placeholder": "p.ej. lain", "placeholder": "p.ej. lain",
@ -19,82 +31,351 @@
"username": "Usuario" "username": "Usuario"
}, },
"nav": { "nav": {
"about": "Sobre",
"back": "Volver",
"chat": "Chat Local", "chat": "Chat Local",
"friend_requests": "Solicitudes de amistad",
"mentions": "Menciones", "mentions": "Menciones",
"dms": "Mensajes Directo",
"public_tl": "Línea Temporal Pública", "public_tl": "Línea Temporal Pública",
"timeline": "Línea Temporal", "timeline": "Línea Temporal",
"twkn": "Toda La Red Conocida" "twkn": "Toda La Red Conocida",
"user_search": "Búsqueda de Usuarios",
"who_to_follow": "A quién seguir",
"preferences": "Preferencias"
}, },
"notifications": { "notifications": {
"broken_favorite": "Estado desconocido, buscándolo...",
"favorited_you": "le gusta tu estado",
"followed_you": "empezó a seguirte", "followed_you": "empezó a seguirte",
"load_older": "Cargar notificaciones antiguas",
"notifications": "Notificaciones", "notifications": "Notificaciones",
"read": "¡Leído!" "read": "¡Leído!",
"repeated_you": "repite tu estado",
"no_more_notifications": "No hay más notificaciones"
}, },
"post_status": { "post_status": {
"new_status": "Post new status",
"account_not_locked_warning": "Tu cuenta no está {0}. Cualquiera puede seguirte y leer las entradas para Solo-Seguidores.",
"account_not_locked_warning_link": "bloqueada",
"attachments_sensitive": "Contenido sensible",
"content_type": {
"plain_text": "Texto Plano"
},
"content_warning": "Tema (opcional)",
"default": "Acabo de aterrizar en L.A.", "default": "Acabo de aterrizar en L.A.",
"posting": "Publicando" "direct_warning": "Esta entrada solo será visible para los usuarios mencionados.",
"posting": "Publicando",
"scope": {
"direct": "Directo - Solo para los usuarios mencionados.",
"private": "Solo-Seguidores - Solo tus seguidores leeran la entrada",
"public": "Público - Entradas visibles en las Líneas Temporales Públicas",
"unlisted": "Sin Listar - Entradas no visibles en las Líneas Temporales Públicas"
}
}, },
"registration": { "registration": {
"bio": "Biografía", "bio": "Biografía",
"email": "Correo electrónico", "email": "Correo electrónico",
"fullname": "Nombre a mostrar", "fullname": "Nombre a mostrar",
"password_confirm": "Confirmación de contraseña", "password_confirm": "Confirmación de contraseña",
"registration": "Registro" "registration": "Registro",
"token": "Token de invitación",
"captcha": "CAPTCHA",
"new_captcha": "Click en la imagen para obtener un nuevo captca",
"validations": {
"username_required": "no puede estar vacío",
"fullname_required": "no puede estar vacío",
"email_required": "no puede estar vacío",
"password_required": "no puede estar vacío",
"password_confirmation_required": "no puede estar vacío",
"password_confirmation_match": "la contraseña no coincide"
}
}, },
"settings": { "settings": {
"attachmentRadius": "Adjuntos",
"attachments": "Adjuntos", "attachments": "Adjuntos",
"autoload": "Activar carga automática al llegar al final de la página", "autoload": "Activar carga automática al llegar al final de la página",
"avatar": "Avatar", "avatar": "Avatar",
"background": "Segundo plano", "avatarAltRadius": "Avatares (Notificaciones)",
"avatarRadius": "Avatares",
"background": "Fondo",
"bio": "Biografía", "bio": "Biografía",
"btnRadius": "Botones",
"cBlue": "Azul (Responder, seguir)",
"cGreen": "Verde (Retweet)",
"cOrange": "Naranja (Favorito)",
"cRed": "Rojo (Cancelar)",
"change_password": "Cambiar contraseña",
"change_password_error": "Hubo un problema cambiando la contraseña.",
"changed_password": "Contraseña cambiada correctamente!",
"collapse_subject": "Colapsar entradas con tema",
"composing": "Redactando",
"confirm_new_password": "Confirmar la nueva contraseña",
"current_avatar": "Tu avatar actual", "current_avatar": "Tu avatar actual",
"current_profile_banner": "Cabecera actual", "current_password": "Contraseña actual",
"current_profile_banner": "Tu cabecera actual",
"data_import_export_tab": "Importar / Exportar Datos",
"default_vis": "Alcance de visibilidad por defecto",
"delete_account": "Eliminar la cuenta",
"delete_account_description": "Eliminar para siempre la cuenta y todos los mensajes.",
"delete_account_error": "Hubo un error al eliminar tu cuenta. Si el fallo persiste, ponte en contacto con el administrador de tu instancia.",
"delete_account_instructions": "Escribe tu contraseña para confirmar la eliminación de tu cuenta.",
"avatar_size_instruction": "El tamaño mínimo recomendado para el avatar es de 150X150 píxeles.",
"export_theme": "Exportar tema",
"filtering": "Filtros", "filtering": "Filtros",
"filtering_explanation": "Todos los estados que contengan estas palabras serán silenciados, una por línea", "filtering_explanation": "Todos los estados que contengan estas palabras serán silenciados, una por línea",
"follow_export": "Exportar personas que tú sigues",
"follow_export_button": "Exporta tus seguidores a un archivo csv",
"follow_export_processing": "Procesando, en breve se te preguntará para guardar el archivo",
"follow_import": "Importar personas que tú sigues", "follow_import": "Importar personas que tú sigues",
"follow_import_error": "Error al importal el archivo", "follow_import_error": "Error al importal el archivo",
"follows_imported": "¡Importado! Procesarlos llevará tiempo.", "follows_imported": "¡Importado! Procesarlos llevará tiempo.",
"foreground": "Primer plano", "foreground": "Primer plano",
"general": "General",
"hide_attachments_in_convo": "Ocultar adjuntos en las conversaciones", "hide_attachments_in_convo": "Ocultar adjuntos en las conversaciones",
"hide_attachments_in_tl": "Ocultar adjuntos en la línea temporal", "hide_attachments_in_tl": "Ocultar adjuntos en la línea temporal",
"hide_isp": "Ocultar el panel específico de la instancia",
"preload_images": "Precargar las imágenes",
"use_one_click_nsfw": "Abrir los adjuntos NSFW con un solo click.",
"hide_post_stats": "Ocultar las estadísticas de las entradas (p.ej. el número de favoritos)",
"hide_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)",
"import_followers_from_a_csv_file": "Importar personas que tú sigues apartir de un archivo csv", "import_followers_from_a_csv_file": "Importar personas que tú sigues apartir de un archivo csv",
"links": "Links", "import_theme": "Importar tema",
"inputRadius": "Campos de entrada",
"checkboxRadius": "Casillas de verificación",
"instance_default": "(por defecto: {value})",
"instance_default_simple": "(por defecto)",
"interface": "Interfaz",
"interfaceLanguage": "Idioma",
"invalid_theme_imported": "El archivo importado no es un tema válido de Pleroma. No se han realizado cambios.",
"limited_availability": "No disponible en tu navegador",
"links": "Enlaces",
"lock_account_description": "Restringir el acceso a tu cuenta solo a seguidores admitidos",
"loop_video": "Vídeos en bucle",
"loop_video_silent_only": "Bucle solo en vídeos sin sonido (p.ej. \"gifs\" de Mastodon)",
"play_videos_in_modal": "Reproducir los vídeos directamente en el visor de medios",
"use_contain_fit": "No recortar los adjuntos en miniaturas",
"name": "Nombre", "name": "Nombre",
"name_bio": "Nombre y Biografía", "name_bio": "Nombre y Biografía",
"new_password": "Nueva contraseña",
"notification_visibility": "Tipos de notificaciones a mostrar",
"notification_visibility_follows": "Nuevos seguidores",
"notification_visibility_likes": "Me gustan (Likes)",
"notification_visibility_mentions": "Menciones",
"notification_visibility_repeats": "Repeticiones (Repeats)",
"no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas",
"hide_network_description": "No mostrar a quién sigo, ni quién me sigue",
"nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW", "nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
"panelRadius": "Paneles",
"pause_on_unfocused": "Parar la transmisión cuando no estés en foco.",
"presets": "Por defecto", "presets": "Por defecto",
"profile_background": "Fondo del Perfil", "profile_background": "Fondo del Perfil",
"profile_banner": "Cabecera del perfil", "profile_banner": "Cabecera del Perfil",
"reply_link_preview": "Activar la previsualización del enlace de responder al pasar el ratón por encima", "profile_tab": "Perfil",
"radii_help": "Estable el redondeo de las esquinas del interfaz (en píxeles)",
"replies_in_timeline": "Réplicas en la línea temporal",
"reply_link_preview": "Activar la previsualización del enlace de responder al pasar el ratón por encim",
"reply_visibility_all": "Mostrar todas las réplicas",
"reply_visibility_following": "Solo mostrar réplicas para mí o usuarios a los que sigo",
"reply_visibility_self": "Solo mostrar réplicas para mí",
"saving_err": "Error al guardar los ajustes",
"saving_ok": "Ajustes guardados",
"security_tab": "Seguridad",
"scope_copy": "Copiar la visibilidad cuando contestamos (En los mensajes directos (MDs) siempre se copia)",
"set_new_avatar": "Cambiar avatar", "set_new_avatar": "Cambiar avatar",
"set_new_profile_background": "Cambiar fondo del perfil", "set_new_profile_background": "Cambiar fondo del perfil",
"set_new_profile_banner": "Cambiar cabecera", "set_new_profile_banner": "Cambiar cabecera del perfil",
"settings": "Ajustes", "settings": "Ajustes",
"subject_input_always_show": "Mostrar siempre el campo del tema",
"subject_line_behavior": "Copiar el tema en las contestaciones",
"subject_line_email": "Tipo email: \"re: tema\"",
"subject_line_mastodon": "Tipo mastodon: copiar como es",
"subject_line_noop": "No copiar",
"stop_gifs": "Iniciar GIFs al pasar el ratón",
"streaming": "Habilite la transmisión automática de nuevas publicaciones cuando se desplaza hacia la parte superior", "streaming": "Habilite la transmisión automática de nuevas publicaciones cuando se desplaza hacia la parte superior",
"text": "Texto", "text": "Texto",
"theme": "Tema", "theme": "Tema",
"theme_help": "Use códigos de color hexadecimales (#rrggbb) para personalizar su tema de colores.", "theme_help": "Use códigos de color hexadecimales (#rrggbb) para personalizar su tema de colores.",
"user_settings": "Ajustes de Usuario" "theme_help_v2_1": "También puede invalidar los colores y la opacidad de ciertos componentes si activa la casilla de verificación, use el botón \"Borrar todo\" para deshacer los cambios.",
"theme_help_v2_2": "Los iconos debajo de algunas entradas son indicadores de contraste de fondo/texto, desplace el ratón para obtener información detallada. Tenga en cuenta que cuando se utilizan indicadores de contraste de transparencia se muestra el peor caso posible.",
"tooltipRadius": "Información/alertas",
"user_settings": "Ajustes de Usuario",
"values": {
"false": "no",
"true": "sí"
},
"notifications": "Notificaciones",
"enable_web_push_notifications": "Habilitar las notificiaciones en el navegador",
"style": {
"switcher": {
"keep_color": "Mantener colores",
"keep_shadows": "Mantener sombras",
"keep_opacity": "Mantener opacidad",
"keep_roundness": "Mantener redondeces",
"keep_fonts": "Mantener fuentes",
"save_load_hint": "Las opciones \"Mantener\" conservan las opciones configuradas actualmente al seleccionar o cargar temas, también almacena dichas opciones al exportar un tema. Cuando se desactiven todas las casillas de verificación, el tema de exportación lo guardará todo.",
"reset": "Reiniciar",
"clear_all": "Limpiar todo",
"clear_opacity": "Limpiar opacidad"
},
"common": {
"color": "Color",
"opacity": "Opacidad",
"contrast": {
"hint": "El ratio de contraste es {ratio}. {level} {context}",
"level": {
"aa": "Cumple con la pauta de nivel AA (mínimo)",
"aaa": "Cumple con la pauta de nivel AAA (recomendado)",
"bad": "No cumple con las pautas de accesibilidad"
},
"context": {
"18pt": "para textos grandes (+18pt)",
"text": "para textos"
}
}
},
"common_colors": {
"_tab_label": "Común",
"main": "Colores comunes",
"foreground_hint": "Vea la pestaña \"Avanzado\" para un control más detallado",
"rgbo": "Iconos, acentos, insignias"
},
"advanced_colors": {
"_tab_label": "Avanzado",
"alert": "Fondo de Alertas",
"alert_error": "Error",
"badge": "Fondo de Insignias",
"badge_notification": "Notificaciones",
"panel_header": "Cabecera del panel",
"top_bar": "Barra superior",
"borders": "Bordes",
"buttons": "Botones",
"inputs": "Campos de entrada",
"faint_text": "Texto desvanecido"
},
"radii": {
"_tab_label": "Redondez"
},
"shadows": {
"_tab_label": "Sombra e iluminación",
"component": "Componente",
"override": "Sobreescribir",
"shadow_id": "Sombra #{value}",
"blur": "Difuminar",
"spread": "Cantidad",
"inset": "Insertada",
"hint": "Para las sombras, también puede usar --variable como un valor de color para usar las variables CSS3. Tenga en cuenta que establecer la opacidad no funcionará en este caso.",
"filter_hint": {
"always_drop_shadow": "Advertencia, esta sombra siempre usa {0} cuando el navegador lo soporta.",
"drop_shadow_syntax": "{0} no soporta el parámetro {1} y la palabra clave {2}.",
"avatar_inset": "Tenga en cuenta que la combinación de sombras insertadas como no-insertadas en los avatares, puede dar resultados inesperados con los avatares transparentes.",
"spread_zero": "Sombras con una cantidad > 0 aparecerá como si estuviera puesto a cero",
"inset_classic": "Las sombras insertadas estarán usando {0}"
},
"components": {
"panel": "Panel",
"panelHeader": "Cabecera del panel",
"topBar": "Barra superior",
"avatar": "Avatar del usuario (en la vista del perfil)",
"avatarStatus": "Avatar del usuario (en la vista de la entrada)",
"popup": "Ventanas y textos emergentes (popups & tooltips)",
"button": "Botones",
"buttonHover": "Botón (encima)",
"buttonPressed": "Botón (presionado)",
"buttonPressedHover": "Botón (presionado+encima)",
"input": "Campo de entrada"
}
},
"fonts": {
"_tab_label": "Fuentes",
"help": "Seleccione la fuente para utilizar para los elementos de la interfaz de usuario. Para \"personalizado\", debe ingresar el nombre exacto de la fuente tal como aparece en el sistema.",
"components": {
"interface": "Interfaz",
"input": "Campos de entrada",
"post": "Texto de publicaciones",
"postCode": "Texto monoespaciado en publicación (texto enriquecido)"
},
"family": "Nombre de la fuente",
"size": "Tamaño (en px)",
"weight": "Peso (negrita)",
"custom": "Personalizado"
},
"preview": {
"header": "Vista previa",
"content": "Contenido",
"error": "Ejemplo de error",
"button": "Botón",
"text": "Un montón de {0} y {1}",
"mono": "contenido",
"input": "Acaba de aterrizar en L.A.",
"faint_link": "manual útil",
"fine_print": "¡Lea nuestro {0} para aprender nada útil!",
"header_faint": "Esto está bien",
"checkbox": "He revisado los términos y condiciones",
"link": "un bonito enlace"
}
}
}, },
"timeline": { "timeline": {
"collapse": "Colapsar",
"conversation": "Conversación", "conversation": "Conversación",
"error_fetching": "Error al cargar las actualizaciones", "error_fetching": "Error al cargar las actualizaciones",
"load_older": "Cargar actualizaciones anteriores", "load_older": "Cargar actualizaciones anteriores",
"no_retweet_hint": "La publicación está marcada como solo para seguidores o directa y no se puede repetir",
"repeated": "repetida",
"show_new": "Mostrar lo nuevo", "show_new": "Mostrar lo nuevo",
"up_to_date": "Actualizado" "up_to_date": "Actualizado",
"no_more_statuses": "No hay más estados"
}, },
"user_card": { "user_card": {
"approve": "Aprovar",
"block": "Bloquear", "block": "Bloquear",
"blocked": "¡Bloqueado!", "blocked": "¡Bloqueado!",
"deny": "Denegar",
"favorites": "Favoritos",
"follow": "Seguir", "follow": "Seguir",
"follow_sent": "¡Solicitud enviada!",
"follow_progress": "Solicitando…",
"follow_again": "¿Enviar solicitud de nuevo?",
"follow_unfollow": "Dejar de seguir",
"followees": "Siguiendo", "followees": "Siguiendo",
"followers": "Seguidores", "followers": "Seguidores",
"following": "¡Siguiendo!", "following": "¡Siguiendo!",
"follows_you": "¡Te sigue!", "follows_you": "¡Te sigue!",
"its_you": "¡Eres tú!",
"media": "Media",
"mute": "Silenciar", "mute": "Silenciar",
"muted": "Silenciado", "muted": "Silenciado",
"per_day": "por día", "per_day": "por día",
"remote_follow": "Seguir", "remote_follow": "Seguir",
"statuses": "Estados" "statuses": "Estados"
},
"user_profile": {
"timeline_title": "Linea temporal del usuario"
},
"who_to_follow": {
"more": "Más",
"who_to_follow": "A quién seguir"
},
"tool_tip": {
"media_upload": "Subir Medios",
"repeat": "Repetir",
"reply": "Contestar",
"favorite": "Favorito",
"user_settings": "Ajustes de usuario"
},
"upload":{
"error": {
"base": "Subida fallida.",
"file_too_big": "Archivo demasiado grande [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Inténtalo más tarde"
},
"file_size_units": {
"B": "B",
"KiB": "KiB",
"MiB": "MiB",
"GiB": "GiB",
"TiB": "TiB"
}
} }
} }

View File

@ -1,93 +1,267 @@
{ {
"chat": {
"title": "Chat"
},
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
"media_proxy": "Media-välityspalvelin",
"scope_options": "Näkyvyyden rajaus",
"text_limit": "Tekstin pituusraja",
"title": "Ominaisuudet",
"who_to_follow": "Seurausehdotukset"
},
"finder": { "finder": {
"error_fetching_user": "Virhe hakiessa käyttäjää", "error_fetching_user": "Virhe hakiessa käyttäjää",
"find_user": "Hae käyttäjä" "find_user": "Hae käyttäjä"
}, },
"general": { "general": {
"apply": "Aseta", "apply": "Aseta",
"submit": "Lähetä" "submit": "Lähetä",
"more": "Lisää",
"generic_error": "Virhe tapahtui"
}, },
"login": { "login": {
"login": "Kirjaudu sisään", "login": "Kirjaudu sisään",
"description": "Kirjaudu sisään OAuthilla",
"logout": "Kirjaudu ulos", "logout": "Kirjaudu ulos",
"password": "Salasana", "password": "Salasana",
"placeholder": "esim. lain", "placeholder": "esim. Seppo",
"register": "Rekisteröidy", "register": "Rekisteröidy",
"username": "Käyttäjänimi" "username": "Käyttäjänimi"
}, },
"nav": { "nav": {
"about": "Tietoja",
"back": "Takaisin",
"chat": "Paikallinen Chat",
"friend_requests": "Seurauspyynnöt",
"mentions": "Maininnat", "mentions": "Maininnat",
"dms": "Yksityisviestit",
"public_tl": "Julkinen Aikajana", "public_tl": "Julkinen Aikajana",
"timeline": "Aikajana", "timeline": "Aikajana",
"twkn": "Koko Tunnettu Verkosto" "twkn": "Koko Tunnettu Verkosto",
"user_search": "Käyttäjähaku",
"who_to_follow": "Seurausehdotukset",
"preferences": "Asetukset"
}, },
"notifications": { "notifications": {
"broken_favorite": "Viestiä ei löydetty...",
"favorited_you": "tykkäsi viestistäsi", "favorited_you": "tykkäsi viestistäsi",
"followed_you": "seuraa sinua", "followed_you": "seuraa sinua",
"load_older": "Lataa vanhempia ilmoituksia",
"notifications": "Ilmoitukset", "notifications": "Ilmoitukset",
"read": "Lue!", "read": "Lue!",
"repeated_you": "toisti viestisi" "repeated_you": "toisti viestisi",
"no_more_notifications": "Ei enempää ilmoituksia"
}, },
"post_status": { "post_status": {
"new_status": "Uusi viesti",
"account_not_locked_warning": "Tilisi ei ole {0}. Kuka vain voi seurata sinua nähdäksesi 'vain-seuraajille' -viestisi",
"account_not_locked_warning_link": "lukittu",
"attachments_sensitive": "Merkkaa liitteet arkaluonteisiksi",
"content_type": {
"plain_text": "Tavallinen teksti"
},
"content_warning": "Aihe (valinnainen)",
"default": "Tulin juuri saunasta.", "default": "Tulin juuri saunasta.",
"posting": "Lähetetään" "direct_warning": "Tämä viesti näkyy vain mainituille käyttäjille.",
"posting": "Lähetetään",
"scope": {
"direct": "Yksityisviesti - Näkyy vain mainituille käyttäjille",
"private": "Vain-seuraajille - Näkyy vain seuraajillesi",
"public": "Julkinen - Näkyy julkisilla aikajanoilla",
"unlisted": "Listaamaton - Ei näy julkisilla aikajanoilla"
}
}, },
"registration": { "registration": {
"bio": "Kuvaus", "bio": "Kuvaus",
"email": "Sähköposti", "email": "Sähköposti",
"fullname": "Koko nimi", "fullname": "Koko nimi",
"password_confirm": "Salasanan vahvistaminen", "password_confirm": "Salasanan vahvistaminen",
"registration": "Rekisteröityminen" "registration": "Rekisteröityminen",
"token": "Kutsuvaltuus",
"captcha": "Varmenne",
"new_captcha": "Paina kuvaa saadaksesi uuden varmenteen",
"validations": {
"username_required": "ei voi olla tyhjä",
"fullname_required": "ei voi olla tyhjä",
"email_required": "ei voi olla tyhjä",
"password_required": "ei voi olla tyhjä",
"password_confirmation_required": "ei voi olla tyhjä",
"password_confirmation_match": "pitää vastata salasanaa"
}
}, },
"settings": { "settings": {
"attachmentRadius": "Liitteet",
"attachments": "Liitteet", "attachments": "Liitteet",
"autoload": "Lataa vanhempia viestejä automaattisesti ruudun pohjalla", "autoload": "Lataa vanhempia viestejä automaattisesti ruudun pohjalla",
"avatar": "Profiilikuva", "avatar": "Profiilikuva",
"avatarAltRadius": "Profiilikuvat (ilmoitukset)",
"avatarRadius": "Profiilikuvat",
"background": "Tausta", "background": "Tausta",
"bio": "Kuvaus", "bio": "Kuvaus",
"btnRadius": "Napit",
"cBlue": "Sininen (Vastaukset, seuraukset)",
"cGreen": "Vihreä (Toistot)",
"cOrange": "Oranssi (Tykkäykset)",
"cRed": "Punainen (Peruminen)",
"change_password": "Vaihda salasana",
"change_password_error": "Virhe vaihtaessa salasanaa.",
"changed_password": "Salasana vaihdettu!",
"collapse_subject": "Minimoi viestit, joille on asetettu aihe",
"composing": "Viestien laatiminen",
"confirm_new_password": "Vahvista uusi salasana",
"current_avatar": "Nykyinen profiilikuvasi", "current_avatar": "Nykyinen profiilikuvasi",
"current_password": "Nykyinen salasana",
"current_profile_banner": "Nykyinen julisteesi", "current_profile_banner": "Nykyinen julisteesi",
"data_import_export_tab": "Tietojen tuonti / vienti",
"default_vis": "Oletusnäkyvyysrajaus",
"delete_account": "Poista tili",
"delete_account_description": "Poista tilisi ja viestisi pysyvästi.",
"delete_account_error": "Virhe poistaessa tiliäsi. Jos virhe jatkuu, ota yhteyttä palvelimesi ylläpitoon.",
"delete_account_instructions": "Syötä salasanasi vahvistaaksesi tilin poiston.",
"export_theme": "Tallenna teema",
"filtering": "Suodatus", "filtering": "Suodatus",
"filtering_explanation": "Kaikki viestit, jotka sisältävät näitä sanoja, suodatetaan. Yksi sana per rivi.", "filtering_explanation": "Kaikki viestit, jotka sisältävät näitä sanoja, suodatetaan. Yksi sana per rivi.",
"follow_export": "Seurausten vienti",
"follow_export_button": "Vie seurauksesi CSV-tiedostoon",
"follow_export_processing": "Käsitellään, sinua pyydetään lataamaan tiedosto hetken päästä",
"follow_import": "Seurausten tuonti",
"follow_import_error": "Virhe tuodessa seuraksia",
"follows_imported": "Seuraukset tuotu! Niiden käsittely vie hetken.",
"foreground": "Korostus", "foreground": "Korostus",
"general": "Yleinen",
"hide_attachments_in_convo": "Piilota liitteet keskusteluissa", "hide_attachments_in_convo": "Piilota liitteet keskusteluissa",
"hide_attachments_in_tl": "Piilota liitteet aikajanalla", "hide_attachments_in_tl": "Piilota liitteet aikajanalla",
"hide_isp": "Piilota palvelimenkohtainen ruutu",
"preload_images": "Esilataa kuvat",
"use_one_click_nsfw": "Avaa NSFW-liitteet yhdellä painalluksella",
"hide_post_stats": "Piilota viestien statistiikka (esim. tykkäysten määrä)",
"hide_user_stats": "Piilota käyttäjien statistiikka (esim. seuraajien määrä)",
"import_followers_from_a_csv_file": "Tuo seuraukset CSV-tiedostosta",
"import_theme": "Tuo tallennettu teema",
"inputRadius": "Syöttökentät",
"checkboxRadius": "Valintalaatikot",
"instance_default": "(oletus: {value})",
"instance_default_simple": "(oletus)",
"interface": "Käyttöliittymä",
"interfaceLanguage": "Käyttöliittymän kieli",
"invalid_theme_imported": "Tuotu tallennettu teema on epäkelpo, muutoksia ei tehty nykyiseen teemaasi.",
"limited_availability": "Ei saatavilla selaimessasi",
"links": "Linkit", "links": "Linkit",
"lock_account_description": "Vain erikseen hyväksytyt käyttäjät voivat seurata tiliäsi",
"loop_video": "Uudelleentoista videot",
"loop_video_silent_only": "Uudelleentoista ainoastaan äänettömät videot (Video-\"giffit\")",
"play_videos_in_modal": "Toista videot modaalissa",
"use_contain_fit": "Älä rajaa liitteitä esikatselussa",
"name": "Nimi", "name": "Nimi",
"name_bio": "Nimi ja kuvaus", "name_bio": "Nimi ja kuvaus",
"nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse.", "new_password": "Uusi salasana",
"notification_visibility": "Ilmoitusten näkyvyys",
"notification_visibility_follows": "Seuraukset",
"notification_visibility_likes": "Tykkäykset",
"notification_visibility_mentions": "Maininnat",
"notification_visibility_repeats": "Toistot",
"no_rich_text_description": "Älä näytä tekstin muotoilua.",
"hide_network_description": "Älä näytä seurauksiani tai seuraajiani",
"nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse",
"panelRadius": "Ruudut",
"pause_on_unfocused": "Pysäytä automaattinen viestien näyttö välilehden ollessa pois fokuksesta",
"presets": "Valmiit teemat", "presets": "Valmiit teemat",
"profile_background": "Taustakuva", "profile_background": "Taustakuva",
"profile_banner": "Juliste", "profile_banner": "Juliste",
"profile_tab": "Profiili",
"radii_help": "Aseta reunojen pyöristys (pikseleinä)",
"replies_in_timeline": "Keskustelut aikajanalla",
"reply_link_preview": "Keskusteluiden vastauslinkkien esikatselu", "reply_link_preview": "Keskusteluiden vastauslinkkien esikatselu",
"reply_visibility_all": "Näytä kaikki vastaukset",
"reply_visibility_following": "Näytä vain vastaukset minulle tai seuraamilleni käyttäjille",
"reply_visibility_self": "Näytä vain vastaukset minulle",
"saving_err": "Virhe tallentaessa asetuksia",
"saving_ok": "Asetukset tallennettu",
"security_tab": "Tietoturva",
"scope_copy": "Kopioi näkyvyysrajaus vastatessa (Yksityisviestit aina kopioivat)",
"set_new_avatar": "Aseta uusi profiilikuva", "set_new_avatar": "Aseta uusi profiilikuva",
"set_new_profile_background": "Aseta uusi taustakuva", "set_new_profile_background": "Aseta uusi taustakuva",
"set_new_profile_banner": "Aseta uusi juliste", "set_new_profile_banner": "Aseta uusi juliste",
"settings": "Asetukset", "settings": "Asetukset",
"subject_input_always_show": "Näytä aihe-kenttä",
"subject_line_behavior": "Aihe-kentän kopiointi",
"subject_line_email": "Kuten sähköposti: \"re: aihe\"",
"subject_line_mastodon": "Kopioi sellaisenaan",
"subject_line_noop": "Älä kopioi",
"stop_gifs": "Toista giffit vain kohdistaessa",
"streaming": "Näytä uudet viestit automaattisesti ollessasi ruudun huipulla", "streaming": "Näytä uudet viestit automaattisesti ollessasi ruudun huipulla",
"text": "Teksti", "text": "Teksti",
"theme": "Teema", "theme": "Teema",
"theme_help": "Käytä heksadesimaalivärejä muokataksesi väriteemaasi.", "theme_help": "Käytä heksadesimaalivärejä muokataksesi väriteemaasi.",
"user_settings": "Käyttäjän asetukset" "theme_help_v2_1": "Voit asettaa tiettyjen osien värin tai läpinäkyvyyden täyttämällä valintalaatikon, käytä \"Tyhjennä kaikki\"-nappia tyhjentääksesi kaiken.",
"theme_help_v2_2": "Ikonit kenttien alla ovat kontrasti-indikaattoreita, lisätietoa kohdistamalla. Käyttäessä läpinäkyvyyttä ne näyttävät pahimman skenaarion.",
"tooltipRadius": "Ohje- tai huomioviestit",
"user_settings": "Käyttäjän asetukset",
"values": {
"false": "pois päältä",
"true": "päällä"
}
}, },
"timeline": { "timeline": {
"collapse": "Sulje", "collapse": "Sulje",
"conversation": "Keskustelu", "conversation": "Keskustelu",
"error_fetching": "Virhe ladatessa viestejä", "error_fetching": "Virhe ladatessa viestejä",
"load_older": "Lataa vanhempia viestejä", "load_older": "Lataa vanhempia viestejä",
"no_retweet_hint": "Viesti ei ole julkinen, eikä sitä voi toistaa",
"repeated": "toisti", "repeated": "toisti",
"show_new": "Näytä uudet", "show_new": "Näytä uudet",
"up_to_date": "Ajantasalla" "up_to_date": "Ajantasalla",
"no_more_statuses": "Ei enempää viestejä"
}, },
"user_card": { "user_card": {
"approve": "Hyväksy",
"block": "Estä",
"blocked": "Estetty!",
"deny": "Älä hyväksy",
"follow": "Seuraa", "follow": "Seuraa",
"follow_sent": "Pyyntö lähetetty!",
"follow_progress": "Pyydetään...",
"follow_again": "Lähetä pyyntö uudestaan",
"follow_unfollow": "Älä seuraa",
"followees": "Seuraa", "followees": "Seuraa",
"followers": "Seuraajat", "followers": "Seuraajat",
"following": "Seuraat!", "following": "Seuraat!",
"follows_you": "Seuraa sinua!", "follows_you": "Seuraa sinua!",
"its_you": "Sinun tili!",
"mute": "Hiljennä", "mute": "Hiljennä",
"muted": "Hiljennetty", "muted": "Hiljennetty",
"per_day": "päivässä", "per_day": "päivässä",
"remote_follow": "Seuraa muualta",
"statuses": "Viestit" "statuses": "Viestit"
},
"user_profile": {
"timeline_title": "Käyttäjän aikajana"
},
"who_to_follow": {
"more": "Lisää",
"who_to_follow": "Seurausehdotukset"
},
"tool_tip": {
"media_upload": "Lataa tiedosto",
"repeat": "Toista",
"reply": "Vastaa",
"favorite": "Tykkää",
"user_settings": "Käyttäjäasetukset"
},
"upload":{
"error": {
"base": "Lataus epäonnistui.",
"file_too_big": "Tiedosto liian suuri [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Yritä uudestaan myöhemmin"
},
"file_size_units": {
"B": "tavua",
"KiB": "kt",
"MiB": "Mt",
"GiB": "Gt",
"TiB": "Tt"
}
} }
} }

View File

@ -157,7 +157,8 @@
"notification_visibility_mentions": "メンション", "notification_visibility_mentions": "メンション",
"notification_visibility_repeats": "リピート", "notification_visibility_repeats": "リピート",
"no_rich_text_description": "リッチテキストをつかわない", "no_rich_text_description": "リッチテキストをつかわない",
"hide_network_description": "わたしがフォローしているひとと、わたしをフォローしているひとを、みせない", "hide_follows_description": "フォローしている人を表示しない",
"hide_followers_description": "フォローしている人を表示しない",
"nsfw_clickthrough": "NSFWなファイルをかくす", "nsfw_clickthrough": "NSFWなファイルをかくす",
"panelRadius": "パネル", "panelRadius": "パネル",
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる", "pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",

View File

@ -156,7 +156,8 @@
"notification_visibility_mentions": "멘션", "notification_visibility_mentions": "멘션",
"notification_visibility_repeats": "반복", "notification_visibility_repeats": "반복",
"no_rich_text_description": "모든 게시물의 서식을 지우기", "no_rich_text_description": "모든 게시물의 서식을 지우기",
"hide_network_description": "내 팔로우와 팔로워를 숨기기", "hide_follows_description": "내가 팔로우하는 사람을 표시하지 않음",
"hide_followers_description": "나를 따르는 사람을 보여주지 마라.",
"nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화", "nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화",
"panelRadius": "패널", "panelRadius": "패널",
"pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기", "pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기",

View File

@ -24,6 +24,7 @@ const messages = {
ja: require('./ja.json'), ja: require('./ja.json'),
ko: require('./ko.json'), ko: require('./ko.json'),
nb: require('./nb.json'), nb: require('./nb.json'),
nl: require('./nl.json'),
oc: require('./oc.json'), oc: require('./oc.json'),
pl: require('./pl.json'), pl: require('./pl.json'),
pt: require('./pt.json'), pt: require('./pt.json'),

372
src/i18n/nl.json Normal file
View File

@ -0,0 +1,372 @@
{
"chat": {
"title": "Chat"
},
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
"media_proxy": "Media proxy",
"scope_options": "Zichtbaarheidsopties",
"text_limit": "Tekst limiet",
"title": "Features",
"who_to_follow": "Wie te volgen"
},
"finder": {
"error_fetching_user": "Fout tijdens ophalen gebruiker",
"find_user": "Gebruiker zoeken"
},
"general": {
"apply": "toepassen",
"submit": "Verzend"
},
"login": {
"login": "Log in",
"description": "Log in met OAuth",
"logout": "Log uit",
"password": "Wachtwoord",
"placeholder": "bv. lain",
"register": "Registreer",
"username": "Gebruikersnaam"
},
"nav": {
"about": "Over",
"back": "Terug",
"chat": "Locale Chat",
"friend_requests": "Volgverzoek",
"mentions": "Vermeldingen",
"dms": "Directe Berichten",
"public_tl": "Publieke Tijdlijn",
"timeline": "Tijdlijn",
"twkn": "Het Geheel Gekende Netwerk",
"user_search": "Zoek Gebruiker",
"who_to_follow": "Wie te volgen",
"preferences": "Voorkeuren"
},
"notifications": {
"broken_favorite": "Onbekende status, aan het zoeken...",
"favorited_you": "vond je status leuk",
"followed_you": "volgt jou",
"load_older": "Laad oudere meldingen",
"notifications": "Meldingen",
"read": "Gelezen!",
"repeated_you": "Herhaalde je status"
},
"post_status": {
"new_status": "Post nieuwe status",
"account_not_locked_warning": "Je account is niet {0}. Iedereen die je volgt kan enkel-volgers posts lezen.",
"account_not_locked_warning_link": "gesloten",
"attachments_sensitive": "Markeer bijlage als gevoelig",
"content_type": {
"plain_text": "Gewone tekst"
},
"content_warning": "Onderwerp (optioneel)",
"default": "Tijd voor een pauze!",
"direct_warning": "Deze post zal enkel zichtbaar zijn voor de personen die genoemd zijn.",
"posting": "Plaatsen",
"scope": {
"direct": "Direct - Post enkel naar genoemde gebruikers",
"private": "Enkel volgers - Post enkel naar volgers",
"public": "Publiek - Post op publieke tijdlijnen",
"unlisted": "Unlisted - Toon niet op publieke tijdlijnen"
}
},
"registration": {
"bio": "Bio",
"email": "Email",
"fullname": "Weergave naam",
"password_confirm": "Wachtwoord bevestiging",
"registration": "Registratie",
"token": "Uitnodigingstoken",
"captcha": "CAPTCHA",
"new_captcha": "Klik op de afbeelding voor een nieuwe captcha",
"validations": {
"username_required": "moet ingevuld zijn",
"fullname_required": "moet ingevuld zijn",
"email_required": "moet ingevuld zijn",
"password_required": "moet ingevuld zijn",
"password_confirmation_required": "moet ingevuld zijn",
"password_confirmation_match": "komt niet overeen met het wachtwoord"
}
},
"settings": {
"attachmentRadius": "Bijlages",
"attachments": "Bijlages",
"autoload": "Automatisch laden wanneer tot de bodem gescrold inschakelen",
"avatar": "Avatar",
"avatarAltRadius": "Avatars (Meldingen)",
"avatarRadius": "Avatars",
"background": "Achtergrond",
"bio": "Bio",
"btnRadius": "Knoppen",
"cBlue": "Blauw (Antwoord, volgen)",
"cGreen": "Groen (Herhaal)",
"cOrange": "Oranje (Vind ik leuk)",
"cRed": "Rood (Annuleer)",
"change_password": "Verander Wachtwoord",
"change_password_error": "Er was een probleem bij het aanpassen van je wachtwoord.",
"changed_password": "Wachtwoord succesvol aangepast!",
"collapse_subject": "Klap posts met onderwerp in",
"composing": "Samenstellen",
"confirm_new_password": "Bevestig nieuw wachtwoord",
"current_avatar": "Je huidige avatar",
"current_password": "Huidig wachtwoord",
"current_profile_banner": "Je huidige profiel banner",
"data_import_export_tab": "Data Import / Export",
"default_vis": "Standaard zichtbaarheidsscope",
"delete_account": "Verwijder Account",
"delete_account_description": "Verwijder je account en berichten permanent.",
"delete_account_error": "Er was een probleem bij het verwijderen van je account. Indien dit probleem blijft, gelieve de administratie van deze instantie te verwittigen.",
"delete_account_instructions": "Typ je wachtwoord in de input hieronder om het verwijderen van je account te bevestigen.",
"export_theme": "Sla preset op",
"filtering": "Filtering",
"filtering_explanation": "Alle statussen die deze woorden bevatten worden genegeerd, één filter per lijn.",
"follow_export": "Volgers export",
"follow_export_button": "Exporteer je volgers naar een csv file",
"follow_export_processing": "Aan het verwerken, binnen enkele ogenblikken wordt je gevraagd je bestand te downloaden",
"follow_import": "Volgers import",
"follow_import_error": "Fout bij importeren volgers",
"follows_imported": "Volgers geïmporteerd! Het kan even duren om ze allemaal te verwerken.",
"foreground": "Voorgrond",
"general": "Algemeen",
"hide_attachments_in_convo": "Verberg bijlages in conversaties",
"hide_attachments_in_tl": "Verberg bijlages in de tijdlijn",
"hide_isp": "Verberg instantie-specifiek paneel",
"preload_images": "Afbeeldingen voorladen",
"hide_post_stats": "Verberg post statistieken (bv. het aantal vind-ik-leuks)",
"hide_user_stats": "Verberg post statistieken (bv. het aantal volgers)",
"import_followers_from_a_csv_file": "Importeer volgers uit een csv file",
"import_theme": "Laad preset",
"inputRadius": "Invoer velden",
"checkboxRadius": "Checkboxen",
"instance_default": "(standaard: {value})",
"instance_default_simple": "(standaard)",
"interface": "Interface",
"interfaceLanguage": "Interface taal",
"invalid_theme_imported": "Het geselecteerde thema is geen door Pleroma ondersteund thema. Er zijn geen aanpassingen gedaan.",
"limited_availability": "Onbeschikbaar in je browser",
"links": "Links",
"lock_account_description": "Laat volgers enkel toe na expliciete toestemming",
"loop_video": "Speel videos af in een lus",
"loop_video_silent_only": "Speel enkel videos zonder geluid af in een lus (bv. Mastodon's \"gifs\")",
"name": "Naam",
"name_bio": "Naam & Bio",
"new_password": "Nieuw wachtwoord",
"notification_visibility": "Type meldingen die getoond worden",
"notification_visibility_follows": "Volgers",
"notification_visibility_likes": "Vind-ik-leuks",
"notification_visibility_mentions": "Vermeldingen",
"notification_visibility_repeats": "Herhalingen",
"no_rich_text_description": "Strip rich text formattering van alle posts",
"hide_network_description": "Toon niet wie mij volgt en wie ik volg.",
"nsfw_clickthrough": "Schakel doorklikbaar verbergen van NSFW bijlages in",
"panelRadius": "Panelen",
"pause_on_unfocused": "Pauzeer streamen wanneer de tab niet gefocused is",
"presets": "Presets",
"profile_background": "Profiel Achtergrond",
"profile_banner": "Profiel Banner",
"profile_tab": "Profiel",
"radii_help": "Stel afronding van hoeken in de interface in (in pixels)",
"replies_in_timeline": "Antwoorden in tijdlijn",
"reply_link_preview": "Schakel antwoordlink preview in bij over zweven met muisaanwijzer",
"reply_visibility_all": "Toon alle antwoorden",
"reply_visibility_following": "Toon enkel antwoorden naar mij of andere gebruikers gericht",
"reply_visibility_self": "Toon enkel antwoorden naar mij gericht",
"saving_err": "Fout tijdens opslaan van instellingen",
"saving_ok": "Instellingen opgeslagen",
"security_tab": "Veiligheid",
"scope_copy": "Neem scope over bij antwoorden (Directe Berichten blijven altijd Direct)",
"set_new_avatar": "Zet nieuwe avatar",
"set_new_profile_background": "Zet nieuwe profiel achtergrond",
"set_new_profile_banner": "Zet nieuwe profiel banner",
"settings": "Instellingen",
"subject_input_always_show": "Maak onderwerpveld altijd zichtbaar",
"subject_line_behavior": "Kopieer onderwerp bij antwoorden",
"subject_line_email": "Zoals email: \"re: onderwerp\"",
"subject_line_mastodon": "Zoals Mastodon: kopieer zoals het is",
"subject_line_noop": "Kopieer niet",
"stop_gifs": "Speel GIFs af bij zweven",
"streaming": "Schakel automatisch streamen van posts in wanneer tot boven gescrold.",
"text": "Tekst",
"theme": "Thema",
"theme_help": "Gebruik hex color codes (#rrggbb) om je kleurschema te wijzigen.",
"theme_help_v2_1": "Je kan ook de kleur en transparantie van bepaalde componenten overschrijven door de checkbox aan te vinken, gebruik de \"Wis alles\" knop om alle overschrijvingen te annuleren.",
"theme_help_v2_2": "Iconen onder sommige items zijn achtergrond/tekst contrast indicators, zweef er over voor gedetailleerde info. Hou er rekening mee dat bij doorzichtigheid de ergst mogelijke situatie wordt weer gegeven.",
"tooltipRadius": "Gereedschapstips/alarmen",
"user_settings": "Gebruikers Instellingen",
"values": {
"false": "nee",
"true": "ja"
},
"notifications": "Meldingen",
"enable_web_push_notifications": "Schakel web push meldingen in",
"style": {
"switcher": {
"keep_color": "Behoud kleuren",
"keep_shadows": "Behoud schaduwen",
"keep_opacity": "Behoud transparantie",
"keep_roundness": "Behoud afrondingen",
"keep_fonts": "Behoud lettertypes",
"save_load_hint": "\"Behoud\" opties behouden de momenteel ingestelde opties bij het selecteren of laden van thema's, maar slaan ook de genoemde opties op bij het exporteren van een thema. Wanneer alle selectievakjes zijn uitgeschakeld, zal het exporteren van thema's alles opslaan.",
"reset": "Reset",
"clear_all": "Wis alles",
"clear_opacity": "Wis transparantie"
},
"common": {
"color": "Kleur",
"opacity": "Transparantie",
"contrast": {
"hint": "Contrast ratio is {ratio}, {level} {context}",
"level": {
"aa": "voldoet aan de richtlijn van niveau AA (minimum)",
"aaa": "voldoet aan de richtlijn van niveau AAA (aangeraden)",
"bad": "voldoet aan geen enkele toegankelijkheidsrichtlijn"
},
"context": {
"18pt": "voor grote (18pt+) tekst",
"text": "voor tekst"
}
}
},
"common_colors": {
"_tab_label": "Gemeenschappelijk",
"main": "Gemeenschappelijke kleuren",
"foreground_hint": "Zie \"Geavanceerd\" tab voor meer gedetailleerde controle",
"rgbo": "Iconen, accenten, badges"
},
"advanced_colors": {
"_tab_label": "Geavanceerd",
"alert": "Alarm achtergrond",
"alert_error": "Fout",
"badge": "Badge achtergrond",
"badge_notification": "Meldingen",
"panel_header": "Paneel hoofding",
"top_bar": "Top bar",
"borders": "Randen",
"buttons": "Knoppen",
"inputs": "Invoervelden",
"faint_text": "Vervaagde tekst"
},
"radii": {
"_tab_label": "Rondheid"
},
"shadows": {
"_tab_label": "Schaduw en belichting",
"component": "Component",
"override": "Overschrijven",
"shadow_id": "Schaduw #{value}",
"blur": "Vervagen",
"spread": "Spreid",
"inset": "Inzet",
"hint": "Voor schaduw kan je ook --variable gebruiken als een kleur waarde om CSS3 variabelen te gebruiken. Houd er rekening mee dat het instellen van opaciteit in dit geval niet werkt.",
"filter_hint": {
"always_drop_shadow": "Waarschuwing, deze schaduw gebruikt altijd {0} als de browser dit ondersteund.",
"drop_shadow_syntax": "{0} ondersteund niet de {1} parameter en {2} sleutelwoord.",
"avatar_inset": "Houd er rekening mee dat het combineren van zowel inzet and niet-inzet schaduwen op transparante avatars onverwachte resultaten kan opleveren.",
"spread_zero": "Schaduw met spreiding > 0 worden weergegeven alsof ze op nul staan",
"inset_classic": "Inzet schaduw zal {0} gebruiken"
},
"components": {
"panel": "Paneel",
"panelHeader": "Paneel hoofding",
"topBar": "Top bar",
"avatar": "Gebruiker avatar (in profiel weergave)",
"avatarStatus": "Gebruiker avatar (in post weergave)",
"popup": "Popups en gereedschapstips",
"button": "Knop",
"buttonHover": "Knop (zweven)",
"buttonPressed": "Knop (ingedrukt)",
"buttonPressedHover": "Knop (ingedrukt+zweven)",
"input": "Invoerveld"
}
},
"fonts": {
"_tab_label": "Lettertypes",
"help": "Selecteer het lettertype om te gebruiken voor elementen van de UI.Voor \"aangepast\" moet je de exacte naam van het lettertype invoeren zoals die in het systeem wordt weergegeven.",
"components": {
"interface": "Interface",
"input": "Invoervelden",
"post": "Post tekst",
"postCode": "Monospaced tekst in een post (rich text)"
},
"family": "Naam lettertype",
"size": "Grootte (in px)",
"weight": "Gewicht (vetheid)",
"custom": "Aangepast"
},
"preview": {
"header": "Voorvertoning",
"content": "Inhoud",
"error": "Voorbeeld fout",
"button": "Knop",
"text": "Nog een boel andere {0} en {1}",
"mono": "inhoud",
"input": "Tijd voor een pauze!",
"faint_link": "handige gebruikershandleiding",
"fine_print": "Lees onze {0} om niets nuttig te leren!",
"header_faint": "Alles komt goed",
"checkbox": "Ik heb de gebruikersvoorwaarden eens van ver bekeken",
"link": "een link"
}
}
},
"timeline": {
"collapse": "Inklappen",
"conversation": "Conversatie",
"error_fetching": "Fout bij ophalen van updates",
"load_older": "Laad oudere Statussen",
"no_retweet_hint": "Post is gemarkeerd als enkel volgers of direct en kan niet worden herhaald",
"repeated": "herhaalde",
"show_new": "Toon nieuwe",
"up_to_date": "Up-to-date"
},
"user_card": {
"approve": "Goedkeuren",
"block": "Blokkeren",
"blocked": "Geblokkeerd!",
"deny": "Ontzeggen",
"favorites": "Vind-ik-leuks",
"follow": "Volgen",
"follow_sent": "Aanvraag verzonden!",
"follow_progress": "Aanvragen…",
"follow_again": "Aanvraag opnieuw zenden?",
"follow_unfollow": "Stop volgen",
"followees": "Aan het volgen",
"followers": "Volgers",
"following": "Aan het volgen!",
"follows_you": "Volgt jou!",
"its_you": "'t is jij!",
"mute": "Dempen",
"muted": "Gedempt",
"per_day": "per dag",
"remote_follow": "Volg vanop afstand",
"statuses": "Statussen"
},
"user_profile": {
"timeline_title": "Gebruikers Tijdlijn"
},
"who_to_follow": {
"more": "Meer",
"who_to_follow": "Wie te volgen"
},
"tool_tip": {
"media_upload": "Upload Media",
"repeat": "Herhaal",
"reply": "Antwoord",
"favorite": "Vind-ik-leuk",
"user_settings": "Gebruikers Instellingen"
},
"upload":{
"error": {
"base": "Upload gefaald.",
"file_too_big": "Bestand is te groot [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Probeer later opnieuw"
},
"file_size_units": {
"B": "B",
"KiB": "KiB",
"MiB": "MiB",
"GiB": "GiB",
"TiB": "TiB"
}
}
}

View File

@ -127,7 +127,8 @@
"notification_visibility_mentions": "Упоминания", "notification_visibility_mentions": "Упоминания",
"notification_visibility_repeats": "Повторы", "notification_visibility_repeats": "Повторы",
"no_rich_text_description": "Убрать форматирование из всех постов", "no_rich_text_description": "Убрать форматирование из всех постов",
"hide_network_description": "Не показывать кого я читаю и кто меня читает", "hide_follows_description": "Не показывать кого я читаю",
"hide_followers_description": "Не показывать кто читает меня",
"nsfw_clickthrough": "Включить скрытие NSFW вложений", "nsfw_clickthrough": "Включить скрытие NSFW вложений",
"panelRadius": "Панели", "panelRadius": "Панели",
"pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе", "pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",

View File

@ -10,6 +10,7 @@ import apiModule from './modules/api.js'
import configModule from './modules/config.js' import configModule from './modules/config.js'
import chatModule from './modules/chat.js' import chatModule from './modules/chat.js'
import oauthModule from './modules/oauth.js' import oauthModule from './modules/oauth.js'
import mediaViewerModule from './modules/media_viewer.js'
import VueTimeago from 'vue-timeago' import VueTimeago from 'vue-timeago'
import VueI18n from 'vue-i18n' import VueI18n from 'vue-i18n'
@ -62,7 +63,8 @@ createPersistedState(persistedStateOptions).then((persistedState) => {
api: apiModule, api: apiModule,
config: configModule, config: configModule,
chat: chatModule, chat: chatModule,
oauth: oauthModule oauth: oauthModule,
mediaViewer: mediaViewerModule
}, },
plugins: [persistedState, pushNotifications], plugins: [persistedState, pushNotifications],
strict: false // Socket modifies itself, let's ignore this for now. strict: false // Socket modifies itself, let's ignore this for now.

View File

@ -20,6 +20,9 @@ const api = {
removeFetcher (state, {timeline}) { removeFetcher (state, {timeline}) {
delete state.fetchers[timeline] delete state.fetchers[timeline]
}, },
setWsToken (state, token) {
state.wsToken = token
},
setSocket (state, socket) { setSocket (state, socket) {
state.socket = socket state.socket = socket
}, },
@ -51,10 +54,14 @@ const api = {
window.clearInterval(fetcher) window.clearInterval(fetcher)
store.commit('removeFetcher', {timeline}) store.commit('removeFetcher', {timeline})
}, },
initializeSocket (store, token) { setWsToken (store, token) {
store.commit('setWsToken', token)
},
initializeSocket (store) {
// Set up websocket connection // Set up websocket connection
if (!store.state.chatDisabled) { if (!store.state.chatDisabled) {
let socket = new Socket('/socket', {params: {token: token}}) const token = store.state.wsToken
const socket = new Socket('/socket', {params: {token}})
socket.connect() socket.connect()
store.dispatch('initializeChat', socket) store.dispatch('initializeChat', socket)
} }

View File

@ -30,7 +30,8 @@ const defaultState = {
interfaceLanguage: browserLocale, interfaceLanguage: browserLocale,
scopeCopy: undefined, // instance default scopeCopy: undefined, // instance default
subjectLineBehavior: undefined, // instance default subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined // instance default alwaysShowSubjectInput: undefined, // instance default
showFeaturesPanel: true
} }
const config = { const config = {

View File

@ -0,0 +1,39 @@
import fileTypeService from '../services/file_type/file_type.service.js'
const mediaViewer = {
state: {
media: [],
currentIndex: 0,
activated: false
},
mutations: {
setMedia (state, media) {
state.media = media
},
setCurrent (state, index) {
state.activated = true
state.currentIndex = index
},
close (state) {
state.activated = false
}
},
actions: {
setMedia ({ commit }, attachments) {
const media = attachments.filter(attachment => {
const type = fileTypeService.fileType(attachment.mimetype)
return type === 'image' || type === 'video'
})
commit('setMedia', media)
},
setCurrent ({ commit, state }, current) {
const index = state.media.indexOf(current)
commit('setCurrent', index || 0)
},
closeMediaViewer ({ commit }) {
commit('close')
}
}
}
export default mediaViewer

View File

@ -2,7 +2,7 @@ import { remove, slice, each, find, maxBy, minBy, merge, last, isArray } from 'l
import apiService from '../services/api/api.service.js' import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js' // import parse from '../services/status_parser/status_parser.js'
const emptyTl = () => ({ const emptyTl = (userId = 0) => ({
statuses: [], statuses: [],
statusesObject: {}, statusesObject: {},
faves: [], faves: [],
@ -14,7 +14,7 @@ const emptyTl = () => ({
loading: false, loading: false,
followers: [], followers: [],
friends: [], friends: [],
userId: 0, userId,
flushMarker: 0 flushMarker: 0
}) })
@ -28,6 +28,7 @@ export const defaultState = {
minId: Number.POSITIVE_INFINITY, minId: Number.POSITIVE_INFINITY,
data: [], data: [],
idStore: {}, idStore: {},
loading: false,
error: false error: false
}, },
favorites: new Set(), favorites: new Set(),
@ -319,7 +320,7 @@ export const mutations = {
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status }) each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
}, },
clearTimeline (state, { timeline }) { clearTimeline (state, { timeline }) {
state.timelines[timeline] = emptyTl() state.timelines[timeline] = emptyTl(state.timelines[timeline].userId)
}, },
setFavorited (state, { status, value }) { setFavorited (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id] const newStatus = state.allStatusesObject[status.id]
@ -348,6 +349,9 @@ export const mutations = {
setError (state, { value }) { setError (state, { value }) {
state.error = value state.error = value
}, },
setNotificationsLoading (state, { value }) {
state.notifications.loading = value
},
setNotificationsError (state, { value }) { setNotificationsError (state, { value }) {
state.notifications.error = value state.notifications.error = value
}, },
@ -376,6 +380,9 @@ const statuses = {
setError ({ rootState, commit }, { value }) { setError ({ rootState, commit }, { value }) {
commit('setError', { value }) commit('setError', { value })
}, },
setNotificationsLoading ({ rootState, commit }, { value }) {
commit('setNotificationsLoading', { value })
},
setNotificationsError ({ rootState, commit }, { value }) { setNotificationsError ({ rootState, commit }, { value }) {
commit('setNotificationsError', { value }) commit('setNotificationsError', { value })
}, },

View File

@ -1,5 +1,5 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { compact, map, each, merge } from 'lodash' import { compact, map, each, merge, find } from 'lodash'
import { set } from 'vue' import { set } from 'vue'
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js' import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
import oauthApi from '../services/new_api/oauth' import oauthApi from '../services/new_api/oauth'
@ -52,13 +52,35 @@ export const mutations = {
state.loggingIn = false state.loggingIn = false
}, },
// TODO Clean after ourselves? // TODO Clean after ourselves?
addFriends (state, { id, friends }) { addFriends (state, { id, friends, page }) {
const user = state.usersObject[id] const user = state.usersObject[id]
user.friends = friends each(friends, friend => {
if (!find(user.friends, { id: friend.id })) {
user.friends.push(friend)
}
})
user.friendsPage = page + 1
}, },
addFollowers (state, { id, followers }) { addFollowers (state, { id, followers, page }) {
const user = state.usersObject[id] const user = state.usersObject[id]
user.followers = followers each(followers, follower => {
if (!find(user.followers, { id: follower.id })) {
user.followers.push(follower)
}
})
user.followersPage = page + 1
},
// Because frontend doesn't have a reason to keep these stuff in memory
// outside of viewing someones user profile.
clearFriendsAndFollowers (state, userKey) {
const user = state.usersObject[userKey]
if (!user) {
return
}
user.friends = []
user.followers = []
user.friendsPage = 0
user.followersPage = 0
}, },
addNewUsers (state, users) { addNewUsers (state, users) {
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user)) each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
@ -91,7 +113,9 @@ export const getters = {
userById: state => id => userById: state => id =>
state.users.find(user => user.id === id), state.users.find(user => user.id === id),
userByName: state => name => userByName: state => name =>
state.users.find(user => user.screen_name === name) state.users.find(user => user.screen_name &&
(user.screen_name.toLowerCase() === name.toLowerCase())
)
} }
export const defaultState = { export const defaultState = {
@ -113,13 +137,34 @@ const users = {
store.rootState.api.backendInteractor.fetchUser({ id }) store.rootState.api.backendInteractor.fetchUser({ id })
.then((user) => store.commit('addNewUsers', [user])) .then((user) => store.commit('addNewUsers', [user]))
}, },
addFriends ({ rootState, commit }, { id }) { addFriends ({ rootState, commit }, fetchBy) {
rootState.api.backendInteractor.fetchFriends({ id }) return new Promise((resolve, reject) => {
.then((friends) => commit('addFriends', { id, friends })) const user = rootState.users.usersObject[fetchBy]
const page = user.friendsPage || 1
rootState.api.backendInteractor.fetchFriends({ id: user.id, page })
.then((friends) => {
commit('addFriends', { id: user.id, friends, page })
resolve(friends)
}).catch(() => {
reject()
})
})
}, },
addFollowers ({ rootState, commit }, { id }) { addFollowers ({ rootState, commit }, fetchBy) {
rootState.api.backendInteractor.fetchFollowers({ id }) return new Promise((resolve, reject) => {
.then((followers) => commit('addFollowers', { id, followers })) const user = rootState.users.usersObject[fetchBy]
const page = user.followersPage || 1
rootState.api.backendInteractor.fetchFollowers({ id: user.id, page })
.then((followers) => {
commit('addFollowers', { id: user.id, followers, page })
resolve(followers)
}).catch(() => {
reject()
})
})
},
clearFriendsAndFollowers ({ commit }, userKey) {
commit('clearFriendsAndFollowers', userKey)
}, },
registerPushNotifications (store) { registerPushNotifications (store) {
const token = store.state.currentUser.credentials const token = store.state.currentUser.credentials
@ -222,10 +267,10 @@ const users = {
commit('setBackendInteractor', backendInteractorService(accessToken)) commit('setBackendInteractor', backendInteractorService(accessToken))
if (user.token) { if (user.token) {
store.dispatch('initializeSocket', user.token) store.dispatch('setWsToken', user.token)
} }
// Start getting fresh tweets. // Start getting fresh posts.
store.dispatch('startFetching', 'friends') store.dispatch('startFetching', 'friends')
// Get user mutes and follower info // Get user mutes and follower info

View File

@ -130,7 +130,7 @@ const updateBanner = ({credentials, params}) => {
// description // description
const updateProfile = ({credentials, params}) => { const updateProfile = ({credentials, params}) => {
// Always include these fields, because they might be empty or false // Always include these fields, because they might be empty or false
const fields = ['description', 'locked', 'no_rich_text', 'hide_network'] const fields = ['description', 'locked', 'no_rich_text', 'hide_follows', 'hide_followers']
let url = PROFILE_UPDATE_URL let url = PROFILE_UPDATE_URL
const form = new FormData() const form = new FormData()
@ -247,15 +247,21 @@ const fetchUser = ({id, credentials}) => {
.then((data) => parseUser(data)) .then((data) => parseUser(data))
} }
const fetchFriends = ({id, credentials}) => { const fetchFriends = ({id, page, credentials}) => {
let url = `${FRIENDS_URL}?user_id=${id}` let url = `${FRIENDS_URL}?user_id=${id}`
if (page) {
url = url + `&page=${page}`
}
return fetch(url, { headers: authHeaders(credentials) }) return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json()) .then((data) => data.json())
.then((data) => data.map(parseUser)) .then((data) => data.map(parseUser))
} }
const fetchFollowers = ({id, credentials}) => { const fetchFollowers = ({id, page, credentials}) => {
let url = `${FOLLOWERS_URL}?user_id=${id}` let url = `${FOLLOWERS_URL}?user_id=${id}`
if (page) {
url = url + `&page=${page}`
}
return fetch(url, { headers: authHeaders(credentials) }) return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json()) .then((data) => data.json())
.then((data) => data.map(parseUser)) .then((data) => data.map(parseUser))

View File

@ -10,12 +10,12 @@ const backendInteractorService = (credentials) => {
return apiService.fetchConversation({id, credentials}) return apiService.fetchConversation({id, credentials})
} }
const fetchFriends = ({id}) => { const fetchFriends = ({id, page}) => {
return apiService.fetchFriends({id, credentials}) return apiService.fetchFriends({id, page, credentials})
} }
const fetchFollowers = ({id}) => { const fetchFollowers = ({id, page}) => {
return apiService.fetchFollowers({id, credentials}) return apiService.fetchFollowers({id, page, credentials})
} }
const fetchAllFollowing = ({username}) => { const fetchAllFollowing = ({username}) => {

View File

@ -100,14 +100,21 @@ export const parseUser = (data) => {
output.rights = data.rights output.rights = data.rights
output.no_rich_text = data.no_rich_text output.no_rich_text = data.no_rich_text
output.default_scope = data.default_scope output.default_scope = data.default_scope
output.hide_network = data.hide_network output.hide_follows = data.hide_follows
output.hide_followers = data.hide_followers
output.background_image = data.background_image output.background_image = data.background_image
// on mastoapi this info is contained in a "relationship"
output.following = data.following
// Websocket token
output.token = data.token
} }
output.created_at = new Date(data.created_at) output.created_at = new Date(data.created_at)
output.locked = data.locked output.locked = data.locked
output.followers_count = data.followers_count output.followers_count = data.followers_count
output.statuses_count = data.statuses_count output.statuses_count = data.statuses_count
output.friends = []
output.followers = []
return output return output
} }
@ -211,6 +218,7 @@ export const parseStatus = (data) => {
output.id = String(data.id) output.id = String(data.id)
output.visibility = data.visibility output.visibility = data.visibility
output.card = data.card
output.created_at = new Date(data.created_at) output.created_at = new Date(data.created_at)
// Converting to string, the right way. // Converting to string, the right way.
@ -262,7 +270,7 @@ export const parseNotification = (data) => {
} }
output.created_at = new Date(data.created_at) output.created_at = new Date(data.created_at)
output.id = String(data.id) output.id = data.id
return output return output
} }

View File

@ -1,27 +1,32 @@
const fileType = (typeString) => { // TODO this func might as well take the entire file and use its mimetype
let type = 'unknown' // or the entire service could be just mimetype service that only operates
// on mimetypes and not files. Currently the naming is confusing.
if (typeString.match(/text\/html/)) { const fileType = mimetype => {
type = 'html' if (mimetype.match(/text\/html/)) {
return 'html'
} }
if (typeString.match(/image/)) { if (mimetype.match(/image/)) {
type = 'image' return 'image'
} }
if (typeString.match(/video/)) { if (mimetype.match(/video/)) {
type = 'video' return 'video'
} }
if (typeString.match(/audio/)) { if (mimetype.match(/audio/)) {
type = 'audio' return 'audio'
} }
return type return 'unknown'
} }
const fileMatchesSomeType = (types, file) =>
types.some(type => fileType(file.mimetype) === type)
const fileTypeService = { const fileTypeService = {
fileType fileType,
fileMatchesSomeType
} }
export default fileTypeService export default fileTypeService

View File

@ -0,0 +1,9 @@
export const mentionMatchesUrl = (attention, url) => {
if (url === attention.statusnet_profile_url) {
return true
}
const [namepart, instancepart] = attention.screen_name.split('@')
const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g')
return !!url.match(matchstring)
}

View File

@ -24,6 +24,7 @@ const fetchAndUpdate = ({store, credentials, older = false}) => {
return apiService.fetchTimeline(args) return apiService.fetchTimeline(args)
.then((notifications) => { .then((notifications) => {
update({store, notifications, older}) update({store, notifications, older})
return notifications
}, () => store.dispatch('setNotificationsError', { value: true })) }, () => store.dispatch('setNotificationsError', { value: true }))
.catch(() => store.dispatch('setNotificationsError', { value: true })) .catch(() => store.dispatch('setNotificationsError', { value: true }))
} }

View File

@ -29,12 +29,15 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
args['userId'] = userId args['userId'] = userId
args['tag'] = tag args['tag'] = tag
const numStatusesBeforeFetch = timelineData.statuses.length
return apiService.fetchTimeline(args) return apiService.fetchTimeline(args)
.then((statuses) => { .then((statuses) => {
if (!older && statuses.length >= 20 && !timelineData.loading && timelineData.statuses.length) { if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) {
store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId }) store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId })
} }
update({store, statuses, timeline, showImmediately, userId}) update({store, statuses, timeline, showImmediately, userId})
return statuses
}, () => store.dispatch('setError', { value: true })) }, () => store.dispatch('setError', { value: true }))
} }

View File

@ -8,6 +8,6 @@ const generateProfileLink = (id, screenName, restrictedNicknames) => {
} }
} }
const isExternal = screenName => screenName.includes('@') const isExternal = screenName => screenName && screenName.includes('@')
export default generateProfileLink export default generateProfileLink

View File

@ -221,6 +221,18 @@
"css": "plus", "css": "plus",
"code": 59413, "code": 59413,
"src": "fontawesome" "src": "fontawesome"
},
{
"uid": "41087bc74d4b20b55059c60a33bf4008",
"css": "edit",
"code": 59415,
"src": "fontawesome"
},
{
"uid": "5717236f6134afe2d2a278a5c9b3927a",
"css": "play-circled",
"code": 61764,
"src": "fontawesome"
} }
] ]
} }

View File

@ -22,6 +22,7 @@
.icon-attention:before { content: '\e814'; } /* '' */ .icon-attention:before { content: '\e814'; } /* '' */
.icon-plus:before { content: '\e815'; } /* '' */ .icon-plus:before { content: '\e815'; } /* '' */
.icon-adjust:before { content: '\e816'; } /* '' */ .icon-adjust:before { content: '\e816'; } /* '' */
.icon-edit:before { content: '\e817'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */ .icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */ .icon-spin4:before { content: '\e834'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */ .icon-link-ext:before { content: '\f08e'; } /* '' */
@ -32,6 +33,7 @@
.icon-plus-squared:before { content: '\f0fe'; } /* '' */ .icon-plus-squared:before { content: '\f0fe'; } /* '' */
.icon-reply:before { content: '\f112'; } /* '' */ .icon-reply:before { content: '\f112'; } /* '' */
.icon-lock-open-alt:before { content: '\f13e'; } /* '' */ .icon-lock-open-alt:before { content: '\f13e'; } /* '' */
.icon-play-circled:before { content: '\f144'; } /* '' */
.icon-thumbs-up-alt:before { content: '\f164'; } /* '' */ .icon-thumbs-up-alt:before { content: '\f164'; } /* '' */
.icon-binoculars:before { content: '\f1e5'; } /* '' */ .icon-binoculars:before { content: '\f1e5'; } /* '' */
.icon-user-plus:before { content: '\f234'; } /* '' */ .icon-user-plus:before { content: '\f234'; } /* '' */

File diff suppressed because one or more lines are too long

View File

@ -22,6 +22,7 @@
.icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe814;&nbsp;'); } .icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe814;&nbsp;'); }
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe815;&nbsp;'); } .icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe815;&nbsp;'); }
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); } .icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); }
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe817;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); } .icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); } .icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); } .icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }
@ -32,6 +33,7 @@
.icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0fe;&nbsp;'); } .icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0fe;&nbsp;'); }
.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf112;&nbsp;'); } .icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf112;&nbsp;'); }
.icon-lock-open-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf13e;&nbsp;'); } .icon-lock-open-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf13e;&nbsp;'); }
.icon-play-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf144;&nbsp;'); }
.icon-thumbs-up-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf164;&nbsp;'); } .icon-thumbs-up-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf164;&nbsp;'); }
.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); } .icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); }
.icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf234;&nbsp;'); } .icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf234;&nbsp;'); }

View File

@ -33,6 +33,7 @@
.icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe814;&nbsp;'); } .icon-attention { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe814;&nbsp;'); }
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe815;&nbsp;'); } .icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe815;&nbsp;'); }
.icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); } .icon-adjust { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe816;&nbsp;'); }
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe817;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); } .icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); } .icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); } .icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }
@ -43,6 +44,7 @@
.icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0fe;&nbsp;'); } .icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0fe;&nbsp;'); }
.icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf112;&nbsp;'); } .icon-reply { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf112;&nbsp;'); }
.icon-lock-open-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf13e;&nbsp;'); } .icon-lock-open-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf13e;&nbsp;'); }
.icon-play-circled { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf144;&nbsp;'); }
.icon-thumbs-up-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf164;&nbsp;'); } .icon-thumbs-up-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf164;&nbsp;'); }
.icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); } .icon-binoculars { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1e5;&nbsp;'); }
.icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf234;&nbsp;'); } .icon-user-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf234;&nbsp;'); }

View File

@ -1,11 +1,11 @@
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('../font/fontello.eot?97335193'); src: url('../font/fontello.eot?94672585');
src: url('../font/fontello.eot?97335193#iefix') format('embedded-opentype'), src: url('../font/fontello.eot?94672585#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?97335193') format('woff2'), url('../font/fontello.woff2?94672585') format('woff2'),
url('../font/fontello.woff?97335193') format('woff'), url('../font/fontello.woff?94672585') format('woff'),
url('../font/fontello.ttf?97335193') format('truetype'), url('../font/fontello.ttf?94672585') format('truetype'),
url('../font/fontello.svg?97335193#fontello') format('svg'); url('../font/fontello.svg?94672585#fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('../font/fontello.svg?97335193#fontello') format('svg'); src: url('../font/fontello.svg?94672585#fontello') format('svg');
} }
} }
*/ */
@ -78,6 +78,7 @@
.icon-attention:before { content: '\e814'; } /* '' */ .icon-attention:before { content: '\e814'; } /* '' */
.icon-plus:before { content: '\e815'; } /* '' */ .icon-plus:before { content: '\e815'; } /* '' */
.icon-adjust:before { content: '\e816'; } /* '' */ .icon-adjust:before { content: '\e816'; } /* '' */
.icon-edit:before { content: '\e817'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */ .icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */ .icon-spin4:before { content: '\e834'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */ .icon-link-ext:before { content: '\f08e'; } /* '' */
@ -88,6 +89,7 @@
.icon-plus-squared:before { content: '\f0fe'; } /* '' */ .icon-plus-squared:before { content: '\f0fe'; } /* '' */
.icon-reply:before { content: '\f112'; } /* '' */ .icon-reply:before { content: '\f112'; } /* '' */
.icon-lock-open-alt:before { content: '\f13e'; } /* '' */ .icon-lock-open-alt:before { content: '\f13e'; } /* '' */
.icon-play-circled:before { content: '\f144'; } /* '' */
.icon-thumbs-up-alt:before { content: '\f164'; } /* '' */ .icon-thumbs-up-alt:before { content: '\f164'; } /* '' */
.icon-binoculars:before { content: '\f1e5'; } /* '' */ .icon-binoculars:before { content: '\f1e5'; } /* '' */
.icon-user-plus:before { content: '\f234'; } /* '' */ .icon-user-plus:before { content: '\f234'; } /* '' */

View File

@ -229,11 +229,11 @@ body {
} }
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('./font/fontello.eot?32716429'); src: url('./font/fontello.eot?28736547');
src: url('./font/fontello.eot?32716429#iefix') format('embedded-opentype'), src: url('./font/fontello.eot?28736547#iefix') format('embedded-opentype'),
url('./font/fontello.woff?32716429') format('woff'), url('./font/fontello.woff?28736547') format('woff'),
url('./font/fontello.ttf?32716429') format('truetype'), url('./font/fontello.ttf?28736547') format('truetype'),
url('./font/fontello.svg?32716429#fontello') format('svg'); url('./font/fontello.svg?28736547#fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -331,23 +331,27 @@ body {
<div class="the-icons span3" title="Code: 0xe814"><i class="demo-icon icon-attention">&#xe814;</i> <span class="i-name">icon-attention</span><span class="i-code">0xe814</span></div> <div class="the-icons span3" title="Code: 0xe814"><i class="demo-icon icon-attention">&#xe814;</i> <span class="i-name">icon-attention</span><span class="i-code">0xe814</span></div>
<div class="the-icons span3" title="Code: 0xe815"><i class="demo-icon icon-plus">&#xe815;</i> <span class="i-name">icon-plus</span><span class="i-code">0xe815</span></div> <div class="the-icons span3" title="Code: 0xe815"><i class="demo-icon icon-plus">&#xe815;</i> <span class="i-name">icon-plus</span><span class="i-code">0xe815</span></div>
<div class="the-icons span3" title="Code: 0xe816"><i class="demo-icon icon-adjust">&#xe816;</i> <span class="i-name">icon-adjust</span><span class="i-code">0xe816</span></div> <div class="the-icons span3" title="Code: 0xe816"><i class="demo-icon icon-adjust">&#xe816;</i> <span class="i-name">icon-adjust</span><span class="i-code">0xe816</span></div>
<div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin">&#xe832;</i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div> <div class="the-icons span3" title="Code: 0xe817"><i class="demo-icon icon-edit">&#xe817;</i> <span class="i-name">icon-edit</span><span class="i-code">0xe817</span></div>
</div> </div>
<div class="row"> <div class="row">
<div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin">&#xe832;</i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
<div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin">&#xe834;</i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div> <div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin">&#xe834;</i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div>
<div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext">&#xf08e;</i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div> <div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext">&#xf08e;</i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div>
<div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt">&#xf08f;</i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div> <div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt">&#xf08f;</i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div>
<div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu">&#xf0c9;</i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
</div> </div>
<div class="row"> <div class="row">
<div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu">&#xf0c9;</i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
<div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt">&#xf0e0;</i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div> <div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt">&#xf0e0;</i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div>
<div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty">&#xf0e5;</i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div> <div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty">&#xf0e5;</i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div>
<div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared">&#xf0fe;</i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div> <div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared">&#xf0fe;</i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div>
<div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply">&#xf112;</i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
</div> </div>
<div class="row"> <div class="row">
<div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply">&#xf112;</i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
<div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt">&#xf13e;</i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div> <div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt">&#xf13e;</i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div>
<div class="the-icons span3" title="Code: 0xf144"><i class="demo-icon icon-play-circled">&#xf144;</i> <span class="i-name">icon-play-circled</span><span class="i-code">0xf144</span></div>
<div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt">&#xf164;</i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div> <div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt">&#xf164;</i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div> <div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
<div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus">&#xf234;</i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div> <div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus">&#xf234;</i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div>
</div> </div>

Binary file not shown.

View File

@ -1,7 +1,7 @@
<?xml version="1.0" standalone="no"?> <?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg"> <svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2018 by original authors @ fontello.com</metadata> <metadata>Copyright (C) 2019 by original authors @ fontello.com</metadata>
<defs> <defs>
<font id="fontello" horiz-adv-x="1000" > <font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="857" descent="-143" /> <font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="857" descent="-143" />
@ -52,6 +52,8 @@
<glyph glyph-name="adjust" unicode="&#xe816;" d="M429 53v608q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41z m428 304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" /> <glyph glyph-name="adjust" unicode="&#xe816;" d="M429 53v608q-83 0-153-41t-110-111-41-152 41-152 110-111 153-41z m428 304q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
<glyph glyph-name="edit" unicode="&#xe817;" d="M496 196l64 65-85 85-64-65v-31h53v-54h32z m245 402q-9 9-18 0l-196-196q-9-9 0-18t18 0l196 196q9 9 0 18z m45-331v-106q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-8-8-18-4-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v70q0 7 5 12l36 36q8 8 20 4t11-16z m-54 411l161-160-375-375h-161v160z m248-73l-51-52-161 161 51 52q16 15 38 15t38-15l85-85q16-16 16-38t-16-38z" horiz-adv-x="1000" />
<glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" /> <glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
<glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" /> <glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
@ -72,6 +74,8 @@
<glyph glyph-name="lock-open-alt" unicode="&#xf13e;" d="M589 428q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v179q0 103 74 177t176 73 177-73 73-177q0-14-10-25t-25-11h-36q-14 0-25 11t-11 25q0 59-42 101t-101 42-101-42-41-101v-179h410z" horiz-adv-x="642.9" /> <glyph glyph-name="lock-open-alt" unicode="&#xf13e;" d="M589 428q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v179q0 103 74 177t176 73 177-73 73-177q0-14-10-25t-25-11h-36q-14 0-25 11t-11 25q0 59-42 101t-101 42-101-42-41-101v-179h410z" horiz-adv-x="642.9" />
<glyph glyph-name="play-circled" unicode="&#xf144;" d="M429 786q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m214-460q18 10 18 31t-18 31l-304 178q-17 11-35 1-18-11-18-31v-358q0-20 18-31 9-4 17-4 10 0 18 5z" horiz-adv-x="857.1" />
<glyph glyph-name="thumbs-up-alt" unicode="&#xf164;" d="M143 107q0 15-11 25t-25 11q-15 0-25-11t-11-25q0-15 11-25t25-11q15 0 25 11t11 25z m89 286v-357q0-15-10-25t-26-11h-160q-15 0-25 11t-11 25v357q0 14 11 25t25 10h160q15 0 26-10t10-25z m661 0q0-48-31-83 9-25 9-43 1-42-24-76 9-31 0-66-9-31-31-52 5-62-27-101-36-43-110-44h-72q-37 0-80 9t-68 16-67 22q-69 24-88 25-15 0-25 11t-11 25v357q0 14 10 25t24 11q13 1 42 33t57 67q38 49 56 67 10 10 17 27t10 27 8 34q4 22 7 34t11 29 19 28q10 11 25 11 25 0 46-6t33-15 22-22 14-25 7-28 2-25 1-22q0-21-6-43t-10-33-16-31q-1-4-5-10t-6-13-5-13h155q43 0 75-32t32-75z" horiz-adv-x="928.6" /> <glyph glyph-name="thumbs-up-alt" unicode="&#xf164;" d="M143 107q0 15-11 25t-25 11q-15 0-25-11t-11-25q0-15 11-25t25-11q15 0 25 11t11 25z m89 286v-357q0-15-10-25t-26-11h-160q-15 0-25 11t-11 25v357q0 14 11 25t25 10h160q15 0 26-10t10-25z m661 0q0-48-31-83 9-25 9-43 1-42-24-76 9-31 0-66-9-31-31-52 5-62-27-101-36-43-110-44h-72q-37 0-80 9t-68 16-67 22q-69 24-88 25-15 0-25 11t-11 25v357q0 14 10 25t24 11q13 1 42 33t57 67q38 49 56 67 10 10 17 27t10 27 8 34q4 22 7 34t11 29 19 28q10 11 25 11 25 0 46-6t33-15 22-22 14-25 7-28 2-25 1-22q0-21-6-43t-10-33-16-31q-1-4-5-10t-6-13-5-13h155q43 0 75-32t32-75z" horiz-adv-x="928.6" />
<glyph glyph-name="binoculars" unicode="&#xf1e5;" d="M393 678v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" /> <glyph glyph-name="binoculars" unicode="&#xf1e5;" d="M393 678v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -240,6 +240,15 @@ describe('The Statuses module', () => {
expect(state.timelines.public.visibleStatuses[0].favorited).to.eql(true) expect(state.timelines.public.visibleStatuses[0].favorited).to.eql(true)
}) })
it('keeps userId when clearing user timeline', () => {
const state = cloneDeep(defaultState)
state.timelines.user.userId = 123
mutations.clearTimeline(state, { timeline: 'user' })
expect(state.timelines.user.userId).to.eql(123)
})
describe('notifications', () => { describe('notifications', () => {
it('removes a notification when the notice gets removed', () => { it('removes a notification when the notice gets removed', () => {
const user = { id: '1' } const user = { id: '1' }

View File

@ -45,6 +45,17 @@ describe('The users module', () => {
const expected = { screen_name: 'Guy', id: '1' } const expected = { screen_name: 'Guy', id: '1' }
expect(getters.userByName(state)(name)).to.eql(expected) expect(getters.userByName(state)(name)).to.eql(expected)
}) })
it('returns user with matching screen_name with different case', () => {
const state = {
users: [
{ screen_name: 'guy', id: '1' }
]
}
const name = 'Guy'
const expected = { screen_name: 'guy', id: '1' }
expect(getters.userByName(state)(name)).to.eql(expected)
})
}) })
describe('getUserById', () => { describe('getUserById', () => {

View File

@ -0,0 +1,19 @@
import fileType from 'src/services/file_type/file_type.service.js'
describe('fileType service', () => {
describe('fileMatchesSomeType', () => {
it('should be true when file type is one of the listed', () => {
const file = { mimetype: 'audio/mpeg' }
const types = ['video', 'audio']
expect(fileType.fileMatchesSomeType(types, file)).to.eql(true)
})
it('should be false when files type is not included in type list', () => {
const file = { mimetype: 'audio/mpeg' }
const types = ['image', 'video']
expect(fileType.fileMatchesSomeType(types, file)).to.eql(false)
})
})
})

View File

@ -0,0 +1,63 @@
import * as MentionMatcher from 'src/services/mention_matcher/mention_matcher.js'
const localAttn = () => ({
id: 123,
is_local: true,
name: 'Guy',
screen_name: 'person',
statusnet_profile_url: 'https://instance.com/users/person'
})
const externalAttn = () => ({
id: 123,
is_local: false,
name: 'Guy',
screen_name: 'person@instance.com',
statusnet_profile_url: 'https://instance.com/users/person'
})
describe('MentionMatcher', () => {
describe.only('mentionMatchesUrl', () => {
it('should match local mention', () => {
const attention = localAttn()
const url = 'https://instance.com/users/person'
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
})
it('should not match a local mention with same name but different instance', () => {
const attention = localAttn()
const url = 'https://website.com/users/person'
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
})
it('should match external pleroma mention', () => {
const attention = externalAttn()
const url = 'https://instance.com/users/person'
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
})
it('should not match external pleroma mention with same name but different instance', () => {
const attention = externalAttn()
const url = 'https://website.com/users/person'
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
})
it('should match external mastodon mention', () => {
const attention = externalAttn()
const url = 'https://instance.com/@person'
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
})
it('should not match external mastodon mention with same name but different instance', () => {
const attention = externalAttn()
const url = 'https://website.com/@person'
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
})
})
})