Merge remote-tracking branch 'upstream/develop' into shigusegubu

* upstream/develop: (49 commits)
  linting
  update test names
  confusion
  better handling of attachments
  Linting.
  Don't use referrerpolicy with media proxy.
  update logo
  support for extended fields (for future, doesn't work yet), fix reply bug
  more fields for users
  some more post fields
  support for CW/Subject. fix replies.
  removing unnecessary conversions since it should already be converted in normalizer
  fix indents
  some consistency
  localization strings
  add support for tab-switcher to automatically switch to first tab if asked index is invalid
  fix login and favorites tab...
  Revert "some initial work to make it possible to use "unregistered" timelines, i.e. not" and some stuff to make favorites still work
  forgot the file
  tests for the tests god! bugfixes for bugfixes throne!
  ...
This commit is contained in:
Henry Jameson 2019-01-22 21:00:52 +03:00
commit 657bcf72fb
40 changed files with 4417 additions and 889 deletions

View file

@ -0,0 +1,13 @@
import InstanceSpecificPanel from '../instance_specific_panel/instance_specific_panel.vue'
import FeaturesPanel from '../features_panel/features_panel.vue'
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
const About = {
components: {
InstanceSpecificPanel,
FeaturesPanel,
TermsOfServicePanel
}
}
export default About

View file

@ -0,0 +1,12 @@
<template>
<div class="sidebar">
<instance-specific-panel></instance-specific-panel>
<features-panel></features-panel>
<terms-of-service-panel></terms-of-service-panel>
</div>
</template>
<script src="./about.js" ></script>
<style lang="scss">
</style>

View file

@ -24,6 +24,9 @@ const Attachment = {
StillImage
},
computed: {
referrerpolicy () {
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
},
type () {
return fileTypeService.fileType(this.attachment.mimetype)
},

View file

@ -10,7 +10,7 @@
<a href="#" @click.prevent="toggleHidden()">Hide</a>
</div>
<a v-if="type === 'image' && (!hidden || preloadImage)" class="image-attachment" :class="{'hidden': hidden && preloadImage}" :href="attachment.url" target="_blank" :title="attachment.description">
<StillImage :class="{'small': isSmall}" referrerpolicy="no-referrer" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
<StillImage :class="{'small': isSmall}" :referrerpolicy="referrerPolicy" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
</a>
<video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" @loadeddata="onVideoDataLoad" :src="attachment.url" controls :loop="loopVideo" playsinline></video>

View file

@ -1,5 +1,5 @@
import Conversation from '../conversation/conversation.vue'
import { find, toInteger } from 'lodash'
import { find } from 'lodash'
const conversationPage = {
components: {
@ -7,7 +7,7 @@ const conversationPage = {
},
computed: {
statusoid () {
const id = toInteger(this.$route.params.id)
const id = this.$route.params.id
const statuses = this.$store.state.statuses.allStatuses
const status = find(statuses, {id})

View file

@ -1,9 +1,8 @@
import { reduce, filter, sortBy } from 'lodash'
import { statusType } from '../../modules/statuses.js'
import Status from '../status/status.vue'
const sortAndFilterConversation = (conversation) => {
conversation = filter(conversation, (status) => statusType(status) !== 'retweet')
conversation = filter(conversation, (status) => status.type !== 'retweet')
return sortBy(conversation, 'id')
}
@ -18,10 +17,12 @@ const conversation = {
'collapsable'
],
computed: {
status () { return this.statusoid },
status () {
return this.statusoid
},
conversation () {
if (!this.status) {
return false
return []
}
const conversationId = this.status.statusnet_conversation_id
@ -32,7 +33,9 @@ const conversation = {
replies () {
let i = 1
return reduce(this.conversation, (result, {id, in_reply_to_status_id}) => {
const irid = Number(in_reply_to_status_id)
/* eslint-disable camelcase */
const irid = in_reply_to_status_id
/* eslint-enable camelcase */
if (irid) {
result[irid] = result[irid] || []
result[irid].push({
@ -69,7 +72,6 @@ const conversation = {
}
},
getReplies (id) {
id = Number(id)
return this.replies[id] || []
},
focused (id) {
@ -80,7 +82,7 @@ const conversation = {
}
},
setHighlight (id) {
this.highlight = Number(id)
this.highlight = id
}
}
}

View file

@ -23,6 +23,9 @@ const SideDrawer = {
},
unseenNotificationsCount () {
return this.unseenNotifications.length
},
suggestionsEnabled () {
return this.$store.state.instance.suggestionsEnabled
}
},
methods: {

View file

@ -62,15 +62,25 @@
</ul>
<ul>
<li @click="toggleDrawer">
<router-link :to="{ name: 'user-search'}">
<router-link :to="{ name: 'user-search' }">
{{ $t("nav.user_search") }}
</router-link>
</li>
<li v-if="currentUser && suggestionsEnabled" @click="toggleDrawer">
<router-link :to="{ name: 'who-to-follow' }">
{{ $t("nav.who_to_follow") }}
</router-link>
</li>
<li @click="toggleDrawer">
<router-link :to="{ name: 'settings'}">
<router-link :to="{ name: 'settings' }">
{{ $t("settings.settings") }}
</router-link>
</li>
<li @click="toggleDrawer">
<router-link :to="{ name: 'about'}">
{{ $t("nav.about") }}
</router-link>
</li>
<li v-if="currentUser" @click="toggleDrawer">
<a @click="doLogout" href="#">
{{ $t("login.logout") }}

View file

@ -117,19 +117,7 @@ const Status = {
return lengthScore > 20
},
isReply () {
if (this.status.in_reply_to_status_id) {
return true
}
// For private replies where we can't see the OP, in_reply_to_status_id will be null.
// So instead, check that the post starts with a @mention.
if (this.status.visibility === 'private') {
var textBody = this.status.text
if (this.status.summary !== null) {
textBody = textBody.substring(this.status.summary.length, textBody.length)
}
return textBody.startsWith('@')
}
return false
return !!this.status.in_reply_to_status_id
},
hideReply () {
if (this.$store.state.config.replyVisibility === 'all') {
@ -141,7 +129,7 @@ const Status = {
if (this.status.user.id === this.$store.state.users.currentUser.id) {
return false
}
if (this.status.activity_type === 'repeat') {
if (this.status.type === 'retweet') {
return false
}
var checkFollowing = this.$store.state.config.replyVisibility === 'following'
@ -270,7 +258,7 @@ const Status = {
},
replyEnter (id, event) {
this.showPreview = true
const targetId = Number(id)
const targetId = id
const statuses = this.$store.state.statuses.allStatuses
if (!this.preview) {
@ -295,7 +283,6 @@ const Status = {
},
watch: {
'highlight': function (id) {
id = Number(id)
if (this.status.id === id) {
let rect = this.$el.getBoundingClientRect()
if (rect.top < 100) {

View file

@ -88,7 +88,7 @@
<div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper">
<a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowMore">Show more</a>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html" v-if="!hideSubjectStatus"></div>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary" v-else></div>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary_html" v-else></div>
<a v-if="hideSubjectStatus" href="#" class="cw-status-hider" @click.prevent="toggleShowMore">Show more</a>
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">Show less</a>
</div>

View file

@ -6,18 +6,26 @@ export default Vue.component('tab-switcher', {
name: 'TabSwitcher',
data () {
return {
active: 0
active: this.$slots.default.findIndex(_ => _.tag)
}
},
methods: {
activateTab(index) {
return () => this.active = index;
activateTab (index) {
return () => {
this.active = index
}
}
},
render(h) {
beforeUpdate () {
const currentSlot = this.$slots.default[this.active]
if (!currentSlot.tag) {
this.active = this.$slots.default.findIndex(_ => _.tag)
}
},
render (h) {
const tabs = this.$slots.default
.filter(slot => slot.data)
.map((slot, index) => {
if (!slot.tag) return
const classesTab = ['tab']
const classesWrapper = ['tab-wrapper']
@ -25,20 +33,24 @@ export default Vue.component('tab-switcher', {
classesTab.push('active')
classesWrapper.push('active')
}
return (
<div class={ classesWrapper.join(' ')}>
<button onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
</div>
)
});
const contents = this.$slots.default.filter(_=>_.data).map(( slot, index ) => {
})
const contents = this.$slots.default.map((slot, index) => {
if (!slot.tag) return
const active = index === this.active
return (
<div class={active ? 'active' : 'hidden'}>
{slot}
</div>
)
});
})
return (
<div class="tab-switcher">
<div class="tabs">

View file

@ -0,0 +1,9 @@
const TermsOfServicePanel = {
computed: {
content () {
return this.$store.state.instance.tos
}
}
}
export default TermsOfServicePanel

View file

@ -0,0 +1,18 @@
<template>
<div>
<div class="panel panel-default">
<div class="panel-body">
<div v-html="content" class="tos-content">
</div>
</div>
</div>
</div>
</template>
<script src="./terms_of_service_panel.js" ></script>
<style lang="scss">
.tos-content {
margin: 1em
}
</style>

View file

@ -142,7 +142,7 @@
border-bottom-right-radius: 0;
.panel-heading {
padding: .6em 0;
padding: .5em 0;
text-align: center;
box-shadow: none;
}
@ -226,10 +226,11 @@
}
}
.user-name{
.user-name {
text-overflow: ellipsis;
overflow: hidden;
flex: 1 0 auto;
flex: 1 1 auto;
margin-right: 1em;
}
.user-screen-name {
@ -245,6 +246,10 @@
.dailyAvg {
min-width: 1px;
flex: 0 0 auto;
margin-left: 1em;
font-size: 0.7em;
color: $fallback--text;
color: var(--text, $fallback--text);
}
.handle {
@ -381,11 +386,6 @@
}
}
.dailyAvg {
margin-left: 1em;
font-size: 0.7em;
color: #CCC;
}
.floater {
}
</style>

View file

@ -5,23 +5,32 @@ import Timeline from '../timeline/timeline.vue'
const UserProfile = {
created () {
this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.commit('clearTimeline', { timeline: 'favorites' })
this.$store.dispatch('startFetching', ['user', this.fetchBy])
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
if (!this.user.id) {
this.$store.dispatch('fetchUser', this.fetchBy)
}
},
destroyed () {
this.$store.dispatch('stopFetching', 'user')
this.$store.dispatch('stopFetching', 'favorites')
},
computed: {
timeline () {
return this.$store.state.statuses.timelines.user
},
favorites () {
return this.$store.state.statuses.timelines.favorites
},
userId () {
return this.$route.params.id || this.user.id
},
userName () {
return this.$route.params.name
return this.$route.params.name || this.user.screen_name
},
isUs () {
return this.userId === this.$store.state.users.currentUser.id
},
friends () {
return this.user.friends
@ -62,21 +71,28 @@ const UserProfile = {
}
},
watch: {
// TODO get rid of this copypasta
userName () {
if (this.isExternal) {
return
}
this.$store.dispatch('stopFetching', 'user')
this.$store.dispatch('stopFetching', 'favorites')
this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.dispatch('startFetching', ['user', this.userName])
this.$store.commit('clearTimeline', { timeline: 'favorites' })
this.$store.dispatch('startFetching', ['user', this.fetchBy])
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
},
userId () {
if (!this.isExternal) {
return
}
this.$store.dispatch('stopFetching', 'user')
this.$store.dispatch('stopFetching', 'favorites')
this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.dispatch('startFetching', ['user', this.userId])
this.$store.commit('clearTimeline', { timeline: 'favorites' })
this.$store.dispatch('startFetching', ['user', this.fetchBy])
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
},
user () {
if (this.user.id && !this.user.followers) {

View file

@ -3,7 +3,7 @@
<div v-if="user.id" class="user-profile panel panel-default">
<user-card-content :user="user" :switcher="true" :selected="timeline.viewing"></user-card-content>
<tab-switcher>
<Timeline :label="$t('user_card.statuses')" :embedded="true" :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="userId"/>
<Timeline :label="$t('user_card.statuses')" :embedded="true" :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="fetchBy"/>
<div :label="$t('user_card.followees')">
<div v-if="friends">
<user-card v-for="friend in friends" :key="friend.id" :user="friend" :showFollows="true"></user-card>
@ -20,6 +20,7 @@
<i class="icon-spin3 animate-spin"></i>
</div>
</div>
<Timeline v-if="isUs" :label="$t('user_card.favorites')" :embedded="true" :title="$t('user_profile.favorites_title')" timeline-name="favorites" :timeline="favorites"/>
</tab-switcher>
</div>
<div v-else class="panel user-profile-placeholder">

View file

@ -0,0 +1,48 @@
import apiService from '../../services/api/api.service.js'
import UserCard from '../user_card/user_card.vue'
const WhoToFollow = {
components: {
UserCard
},
data () {
return {
users: []
}
},
mounted () {
this.getWhoToFollow()
},
methods: {
showWhoToFollow (reply) {
reply.forEach((i, index) => {
const user = {
id: 0,
name: i.display_name,
screen_name: i.acct,
profile_image_url: i.avatar || '/images/avi.png'
}
this.users.push(user)
this.$store.state.api.backendInteractor.externalProfile(user.screen_name)
.then((externalUser) => {
if (!externalUser.error) {
this.$store.commit('addNewUsers', [externalUser])
user.id = externalUser.id
}
})
})
},
getWhoToFollow () {
const credentials = this.$store.state.users.currentUser.credentials
if (credentials) {
apiService.suggestions({credentials: credentials})
.then((reply) => {
this.showWhoToFollow(reply)
})
}
}
}
}
export default WhoToFollow

View file

@ -0,0 +1,15 @@
<template>
<div class="panel panel-default">
<div class="panel-heading">
{{$t('who_to_follow.who_to_follow')}}
</div>
<div class="panel-body">
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card>
</div>
</div>
</template>
<script src="./who_to_follow.js"></script>
<style lang="scss">
</style>

View file

@ -50,14 +50,6 @@ const WhoToFollowPanel = {
user: function () {
return this.$store.state.users.currentUser.screen_name
},
moreUrl: function () {
const host = window.location.hostname
const user = this.user
const suggestionsWeb = this.$store.state.instance.suggestionsWeb
const url = suggestionsWeb.replace(/{{host}}/g, encodeURIComponent(host))
.replace(/{{user}}/g, encodeURIComponent(user))
return url
},
suggestionsEnabled () {
return this.$store.state.instance.suggestionsEnabled
}

View file

@ -13,7 +13,7 @@
{{user.name}}
</router-link><br />
</span>
<img v-bind:src="$store.state.instance.logo"> <a v-bind:href="moreUrl" target="_blank">{{$t('who_to_follow.more')}}</a>
<img v-bind:src="$store.state.instance.logo"> <router-link :to="{ name: 'who-to-follow' }">{{$t('who_to_follow.more')}}</router-link>
</div>
</div>
</div>