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

* upstream/develop: (32 commits)
  [Debug] Avoid duplicates in the who to follow panel
  updated German translation  * added theme settings  * added various missing single strings
  Fix translation typo in registration.vue
  Update README
  Fix profiles without statuses not loading
  Fix conflicting styles
  Remove commented out back button
  Cleanup and remove divider element in side drawer
  New routes, notifications, other impovements in side drwaer
  Add "noAttachmentLinks" to src/modules/instance.js
  Make "noAttachmentLinks" configurable
  No attachment links
  Treat reserved users like external users in the frontend.
  User Card Content fixes and updates
  scopeCopy → true by default
  Restore old routes, enable user route as fallback.
  improve web push notifications
  fix
  Update japanese translation
  fix inconsistencies within who_to_follow_panel
  ...
This commit is contained in:
Henry Jameson 2019-01-07 13:23:03 +03:00
commit 1eea45cf6d
57 changed files with 1255 additions and 470 deletions

View file

@ -1,6 +1,7 @@
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const chatPanel = {
props: [ 'floating' ],
data () {
return {
currentMessage: '',
@ -22,7 +23,7 @@ const chatPanel = {
this.collapsed = !this.collapsed
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name)
return generateProfileLink(user.id, user.username, this.$store.state.instance.restrictedNicknames)
}
}
}

View file

@ -1,10 +1,10 @@
<template>
<div class="chat-panel" v-if="!this.collapsed">
<div class="chat-panel" v-if="!this.collapsed || !this.floating">
<div class="panel panel-default">
<div class="panel-heading timeline-heading chat-heading" @click.stop.prevent="togglePanel">
<div class="panel-heading timeline-heading" :class="{ 'chat-heading': floating }" @click.stop.prevent="togglePanel">
<div class="title">
{{$t('chat.title')}}
<i class="icon-cancel" style="float: right;"></i>
<i class="icon-cancel" style="float: right;" v-if="floating"></i>
</div>
</div>
<div class="chat-window" v-chat-scroll>
@ -52,6 +52,7 @@
right: 0px;
bottom: 0px;
z-index: 1000;
max-width: 25em;
}
.chat-heading {
@ -63,10 +64,13 @@
}
.chat-window {
width: 345px;
max-height: 40vh;
overflow-y: auto;
overflow-x: hidden;
max-height: 20em;
}
.chat-window-container {
height: 100%;
}
.chat-message {

View file

@ -32,7 +32,7 @@ const LoginForm = {
.then((result) => {
this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token)
this.$router.push('/~/main/friends')
this.$router.push({name: 'friends'})
})
})
}

View file

@ -1,5 +1,4 @@
const NavPanel = {
props: [ 'activatePanel' ],
computed: {
currentUser () {
return this.$store.state.users.currentUser

View file

@ -3,32 +3,32 @@
<div class="panel panel-default">
<ul>
<li v-if='currentUser'>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'friends' }">
<router-link :to="{ name: 'friends' }">
{{ $t("nav.timeline") }}
</router-link>
</li>
<li v-if='currentUser'>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'mentions', params: { username: currentUser.screen_name } }">
<router-link :to="{ name: 'mentions', params: { username: currentUser.screen_name } }">
{{ $t("nav.mentions") }}
</router-link>
</li>
<li v-if='currentUser'>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
{{ $t("nav.dms") }}
</router-link>
</li>
<li v-if='currentUser && currentUser.locked'>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'friend-requests' }">
<router-link :to="{ name: 'friend-requests' }">
{{ $t("nav.friend_requests") }}
</router-link>
</li>
<li>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'public-timeline' }">
<router-link :to="{ name: 'public-timeline' }">
{{ $t("nav.public_tl") }}
</router-link>
</li>
<li>
<router-link @click.native="activatePanel('timeline')" :to="{ name: 'public-external-timeline' }">
<router-link :to="{ name: 'public-external-timeline' }">
{{ $t("nav.twkn") }}
</router-link>
</li>

View file

@ -11,10 +11,7 @@ const Notification = {
betterShadow: this.$store.state.interface.browserSupport.cssFilter
}
},
props: [
'notification',
'activatePanel'
],
props: [ 'notification' ],
components: {
Status, StillImage, UserCardContent
},
@ -23,7 +20,7 @@ const Notification = {
this.userExpanded = !this.userExpanded
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name)
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
}
},
computed: {

View file

@ -1,5 +1,5 @@
<template>
<status :activatePanel="activatePanel" 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>
<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"/>
@ -25,15 +25,15 @@
<small>{{$t('notifications.followed_you')}}</small>
</span>
</div>
<small class="timeago"><router-link @click.native="activatePanel('timeline')" v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
<small class="timeago"><router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
</span>
<div class="follow-text" v-if="notification.type === 'follow'">
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(notification.action.user)">
<router-link :to="userProfileLink(notification.action.user)">
@{{notification.action.user.screen_name}}
</router-link>
</div>
<template v-else>
<status :activatePanel="activatePanel" class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
<status class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
</template>
</div>
</div>

View file

@ -1,10 +1,12 @@
import Notification from '../notification/notification.vue'
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
import { sortBy, filter } from 'lodash'
import {
notificationsFromStore,
visibleNotificationsFromStore,
unseenNotificationsFromStore
} from '../../services/notification_utils/notification_utils.js'
const Notifications = {
props: [ 'activatePanel' ],
created () {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
@ -12,28 +14,17 @@ const Notifications = {
notificationsFetcher.startFetching({ store, credentials })
},
computed: {
visibleTypes () {
return [
this.$store.state.config.notificationVisibility.likes && 'like',
this.$store.state.config.notificationVisibility.mentions && 'mention',
this.$store.state.config.notificationVisibility.repeats && 'repeat',
this.$store.state.config.notificationVisibility.follows && 'follow'
].filter(_ => _)
},
notifications () {
return this.$store.state.statuses.notifications.data
return notificationsFromStore(this.$store)
},
error () {
return this.$store.state.statuses.notifications.error
},
unseenNotifications () {
return filter(this.visibleNotifications, ({seen}) => !seen)
return unseenNotificationsFromStore(this.$store)
},
visibleNotifications () {
// Don't know why, but sortBy([seen, -action.id]) doesn't work.
let sortedNotifications = sortBy(this.notifications, ({action}) => -action.id)
sortedNotifications = sortBy(sortedNotifications, 'seen')
return sortedNotifications.filter((notification) => this.visibleTypes.includes(notification.type))
return visibleNotificationsFromStore(this.$store)
},
unseenCount () {
return this.unseenNotifications.length

View file

@ -14,7 +14,7 @@
<div class="panel-body">
<div v-for="notification in visibleNotifications" :key="notification.action.id" class="notification" :class='{"unseen": !notification.seen}'>
<div class="notification-overlay"></div>
<notification :activatePanel="activatePanel" :notification="notification"></notification>
<notification :notification="notification"></notification>
</div>
</div>
<div class="panel-footer">

View file

@ -11,7 +11,7 @@ const oac = {
}).then((result) => {
this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token)
this.$router.push('/~/main/friends')
this.$router.push({name: 'friends'})
})
}
}

View file

@ -249,6 +249,7 @@ const PostStatusForm = {
}
this.$emit('posted')
let el = this.$el.querySelector('textarea')
el.style.height = 'auto'
el.style.height = undefined
this.error = null
} else {

View file

@ -28,7 +28,7 @@ const registration = {
},
created () {
if ((!this.registrationOpen && !this.token) || this.signedIn) {
this.$router.push('/~/main/all')
this.$router.push({name: 'root'})
}
this.setCaptcha()
@ -48,15 +48,17 @@ const registration = {
async submit () {
this.user.nickname = this.user.username
this.user.token = this.token
this.user.captcha_solution = this.captcha.solution
this.user.captcha_token = this.captcha.token
this.user.captcha_answer_data = this.captcha.answer_data
this.$v.$touch()
if (!this.$v.$invalid) {
try {
await this.signUp(this.user)
this.$router.push('/~/main/friends')
this.$router.push({name: 'friends'})
} catch (error) {
console.warn('Registration failed: ' + error)
}

View file

@ -76,15 +76,16 @@
</div>
<div class="form-group" id="captcha-group" v-if="captcha.type != 'none'">
<label class='form--label' for='captcha-label'>{{$t('captcha')}}</label>
<template v-if="captcha.type == 'kocaptcha'">
<img v-bind:src="captcha.url" v-on:click="setCaptcha">
<sub>Click the image to get a new captcha</sub>
<label class='form--label' for='captcha-label'>CAPTCHA</label>
<sub>{{$t('registration.new_captcha')}}</sub>
<input :disabled="isPending"
v-model='captcha.solution'
class='form-control' id='captcha-answer' type='text'>
class='form-control' id='captcha-answer' type='text' autocomplete="off">
</template>
</div>

View file

@ -0,0 +1,48 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
// TODO: separate touch gesture stuff into their own utils if more components want them
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY])
const SideDrawer = {
props: [ 'logout' ],
data: () => ({
closed: true,
touchCoord: [0, 0]
}),
components: { UserCardContent },
computed: {
currentUser () {
return this.$store.state.users.currentUser
},
chat () { return this.$store.state.chat.channel.state === 'joined' },
unseenNotifications () {
return unseenNotificationsFromStore(this.$store)
},
unseenNotificationsCount () {
return this.unseenNotifications.length
}
},
methods: {
toggleDrawer () {
this.closed = !this.closed
},
doLogout () {
this.logout()
this.toggleDrawer()
},
touchStart (e) {
this.touchCoord = touchEventCoord(e)
},
touchMove (e) {
const delta = deltaCoord(this.touchCoord, touchEventCoord(e))
if (delta[0] < -30 && Math.abs(delta[1]) < Math.abs(delta[0]) && !this.closed) {
this.toggleDrawer()
}
}
}
}
export default SideDrawer

View file

@ -0,0 +1,189 @@
<template>
<div class="side-drawer-container"
:class="{ 'side-drawer-container-closed': closed, 'side-drawer-container-open': !closed }"
>
<div class="side-drawer"
:class="{'side-drawer-closed': closed}"
@touchstart="touchStart"
@touchmove="touchMove"
>
<div class="side-drawer-heading" @click="toggleDrawer">
<user-card-content :user="currentUser" :switcher="false" :hideBio="true" v-if="currentUser">
</user-card-content>
</div>
<ul>
<li v-if="currentUser" @click="toggleDrawer">
<router-link :to="{ name: 'new-status', params: { username: currentUser.screen_name } }">
{{ $t("post_status.new_status") }}
</router-link>
</li>
<li v-else @click="toggleDrawer">
<router-link :to="{ name: 'login' }">
{{ $t("login.login") }}
</router-link>
</li>
<li v-if="currentUser" @click="toggleDrawer">
<router-link :to="{ name: 'notifications', params: { username: currentUser.screen_name } }">
{{ $t("notifications.notifications") }} {{ unseenNotificationsCount > 0 ? `(${unseenNotificationsCount})` : '' }}
</router-link>
</li>
<li v-if="currentUser" @click="toggleDrawer">
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
{{ $t("nav.dms") }}
</router-link>
</li>
</ul>
<ul>
<li v-if="currentUser" @click="toggleDrawer">
<router-link :to="{ name: 'friends' }">
{{ $t("nav.timeline") }}
</router-link>
</li>
<li v-if="currentUser && currentUser.locked" @click="toggleDrawer">
<router-link to='/friend-requests'>
{{ $t("nav.friend_requests") }}
</router-link>
</li>
<li @click="toggleDrawer">
<router-link to='/main/public'>
{{ $t("nav.public_tl") }}
</router-link>
</li>
<li @click="toggleDrawer">
<router-link to='/main/all'>
{{ $t("nav.twkn") }}
</router-link>
</li>
<li v-if="currentUser && chat" @click="toggleDrawer">
<router-link :to="{ name: 'chat' }">
{{ $t("nav.chat") }}
</router-link>
</li>
</ul>
<ul>
<li @click="toggleDrawer">
<router-link :to="{ name: 'user-search'}">
{{ $t("nav.user_search") }}
</router-link>
</li>
<li @click="toggleDrawer">
<router-link :to="{ name: 'settings'}">
{{ $t("settings.settings") }}
</router-link>
</li>
<li v-if="currentUser" @click="toggleDrawer">
<a @click="doLogout" href="#">
{{ $t("login.logout") }}
</a>
</li>
</ul>
</div>
<div class="side-drawer-click-outside"
@click.stop.prevent="toggleDrawer"
:class="{'side-drawer-click-outside-closed': closed}"
></div>
</div>
</template>
<script src="./side_drawer.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
.side-drawer-container {
position: fixed;
z-index: 1000;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: stretch;
}
.side-drawer-container-open {
transition-delay: 0.0s;
transition-property: left;
}
.side-drawer-container-closed {
left: -100%;
transition-delay: 0.5s;
transition-property: left;
}
.side-drawer-click-outside {
flex: 1 1 100%;
}
.side-drawer {
overflow-x: hidden;
transition: 0.5s;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
margin: 0 0 0 -100px;
padding: 0 0 1em 100px;
width: 80%;
max-width: 20em;
flex: 0 0 80%;
box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.6);
box-shadow: var(--panelShadow);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
}
.side-drawer-click-outside-closed {
flex: 0 0 0;
}
.side-drawer-closed {
margin: 0 0 0 calc(-100% - 100px);
}
.side-drawer-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
display: flex;
min-height: 7em;
padding: 0;
margin: 0;
.profile-panel-background {
border-radius: 0;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
}
}
.side-drawer ul {
list-style: none;
margin: 0;
padding: 0;
border-bottom: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
margin: 0.2em 0;
}
.side-drawer ul:last-child {
border: 0;
}
.side-drawer li {
padding: 0;
a {
display: block;
padding: 0.5em 0.85em;
&:hover {
background-color: $fallback--lightBg;
background-color: var(--lightBg, $fallback--lightBg);
}
}
}
</style>

View file

@ -21,8 +21,7 @@ const Status = {
'replies',
'noReplyLinks',
'noHeading',
'inlineExpanded',
'activatePanel'
'inlineExpanded'
],
data () {
return {
@ -291,7 +290,7 @@ const Status = {
this.showPreview = false
},
userProfileLink (id, name) {
return generateProfileLink(id, name)
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
}
},
watch: {

View file

@ -3,7 +3,7 @@
<template v-if="muted && !noReplyLinks">
<div class="media status container muted">
<small>
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(status.user.id, status.user.screen_name)">
<router-link :to="userProfileLink(status.user.id, status.user.screen_name)">
{{status.user.screen_name}}
</router-link>
</small>
@ -38,12 +38,12 @@
<h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4>
<h4 class="user-name" v-else>{{status.user.name}}</h4>
<span class="links">
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(status.user.id, status.user.screen_name)">
<router-link :to="userProfileLink(status.user.id, status.user.screen_name)">
{{status.user.screen_name}}
</router-link>
<span v-if="status.in_reply_to_screen_name" class="faint reply-info">
<i class="icon-right-open"></i>
<router-link @click.native="activatePanel('timeline')" :to="userProfileLink(status.in_reply_to_user_id, status.in_reply_to_screen_name)">
<router-link :to="userProfileLink(status.in_reply_to_user_id, status.in_reply_to_screen_name)">
{{status.in_reply_to_screen_name}}
</router-link>
</span>
@ -60,7 +60,7 @@
</h4>
</div>
<div class="media-heading-right">
<router-link class="timeago" @click.native="activatePanel('timeline')" :to="{ name: 'conversation', params: { id: status.id } }">
<router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }">
<timeago :since="status.created_at" :auto-update="60"></timeago>
</router-link>
<div class="button-icon visibility-icon" v-if="status.visibility">
@ -79,7 +79,7 @@
</div>
<div v-if="showPreview" class="status-preview-container">
<status :activatePanel="activatePanel" class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status>
<status class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status>
<div class="status-preview status-preview-loading" v-else>
<i class="icon-spin4 animate-spin"></i>
</div>

View file

@ -33,7 +33,7 @@ const UserCard = {
this.$store.dispatch('removeFollowRequest', this.user)
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name)
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
}
}
}

View file

@ -3,7 +3,7 @@ import { hex2rgb } from '../../services/color_convert/color_convert.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
export default {
props: [ 'user', 'switcher', 'selected', 'hideBio', 'activatePanel' ],
props: [ 'user', 'switcher', 'selected', 'hideBio' ],
data () {
return {
followRequestInProgress: false,
@ -180,7 +180,7 @@ export default {
}
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name)
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
}
}
}

View file

@ -2,22 +2,25 @@
<div id="heading" class="profile-panel-background" :style="headingStyle">
<div class="panel-heading text-center">
<div class='user-info'>
<router-link @click.native="activatePanel && activatePanel('timeline')" :to="{ name: 'user-settings' }" style="float: right; margin-top:16px;" v-if="!isOtherUser">
<i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i>
</router-link>
<a :href="user.statusnet_profile_url" target="_blank" class="floater" v-if="isOtherUser">
<i class="icon-link-ext usersettings"></i>
</a>
<div class='container'>
<router-link @click.native="activatePanel && activatePanel('timeline')" :to="userProfileLink(user)">
<router-link :to="userProfileLink(user)">
<StillImage class="avatar" :class='{ "better-shadow": betterShadow }' :src="user.profile_image_url_original"/>
</router-link>
<div class="name-and-screen-name">
<div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
<router-link @click.native="activatePanel && activatePanel('timeline')" class='user-screen-name' :to="userProfileLink(user)">
<span>@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
<span v-if="!hideUserStatsLocal" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
<div class="top-line">
<div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
<div :title="user.name" class='user-name' v-else>{{user.name}}</div>
<router-link :to="{ name: 'user-settings' }" v-if="!isOtherUser">
<i class="button-icon icon-cog usersettings" :title="$t('tool_tip.user_settings')"></i>
</router-link>
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser">
<i class="icon-link-ext usersettings"></i>
</a>
</div>
<router-link class='user-screen-name' :to="userProfileLink(user)">
<span class="handle">@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
<span v-if="!hideUserStatsLocal && !hideBio" class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
</router-link>
</div>
</div>
@ -25,7 +28,7 @@
<div v-if="user.follows_you && loggedIn && isOtherUser" class="following">
{{ $t('user_card.follows_you') }}
</div>
<div class="floater" v-if="isOtherUser && (loggedIn || !switcher)">
<div class="highlighter" v-if="isOtherUser && (loggedIn || !switcher)">
<!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
<input class="userHighlightText" type="text" :id="'userHighlightColorTx'+user.id" v-if="userHighlightType !== 'disabled'" v-model="userHighlightColor"/>
<input class="userHighlightCl" type="color" :id="'userHighlightColor'+user.id" v-if="userHighlightType !== 'disabled'" v-model="userHighlightColor"/>
@ -139,7 +142,7 @@
border-bottom-right-radius: 0;
.panel-heading {
padding: 0.6em 0em;
padding: .6em 0;
text-align: center;
box-shadow: none;
}
@ -158,10 +161,10 @@
.user-info {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
padding: 0 16px;
padding: 0 26px;
.container {
padding: 16px 10px 6px 10px;
padding: 16px 0 6px;
display: flex;
max-height: 56px;
@ -218,11 +221,15 @@
vertical-align: middle;
object-fit: contain
}
.top-line {
display: flex;
}
}
.user-name{
text-overflow: ellipsis;
overflow: hidden;
flex: 1 0 auto;
}
.user-screen-name {
@ -232,27 +239,73 @@
font-weight: light;
font-size: 15px;
padding-right: 0.1em;
width: 100%;
display: flex;
.dailyAvg {
min-width: 1px;
flex: 0 0 auto;
}
.handle {
min-width: 1px;
flex: 0 1 auto;
text-overflow: ellipsis;
overflow: hidden;
}
}
.user-meta {
margin-bottom: .4em;
margin-bottom: .15em;
display: flex;
align-items: baseline;
font-size: 14px;
line-height: 22px;
flex-wrap: wrap;
.following {
font-size: 14px;
flex: 0 0 100%;
flex: 1 0 auto;
margin: 0;
padding-left: 16px;
margin-bottom: .25em;
text-align: left;
float: left;
}
.floater {
margin: 0;
}
&::after {
display: block;
content: '';
clear: both;
.highlighter {
flex: 0 1 auto;
display: flex;
flex-wrap: wrap;
margin-right: -.5em;
align-self: start;
.userHighlightCl {
padding: 2px 10px;
flex: 1 0 auto;
}
.userHighlightSel,
.userHighlightSel.select {
padding-top: 0;
padding-bottom: 0;
flex: 1 0 auto;
}
.userHighlightSel.select i {
line-height: 22px;
}
.userHighlightText {
width: 70px;
flex: 1 0 auto;
}
.userHighlightCl,
.userHighlightText,
.userHighlightSel,
.userHighlightSel.select {
height: 22px;
vertical-align: top;
margin-right: .5em;
margin-bottom: .25em;
}
}
}
.user-interactions {
@ -260,8 +313,13 @@
flex-flow: row wrap;
justify-content: space-between;
margin-right: -.75em;
div {
flex: 1;
flex: 1 0 0;
margin-right: .75em;
margin-bottom: .6em;
white-space: nowrap;
}
.mute {
@ -280,8 +338,9 @@
}
button {
width: 92%;
width: 100%;
height: 100%;
margin: 0;
}
.remote-button {
@ -304,10 +363,11 @@
justify-content: space-between;
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
flex-wrap: wrap;
}
.user-count {
flex: 1;
flex: 1 0 auto;
padding: .5em 0 .5em 0;
margin: 0 .5em;
@ -327,32 +387,5 @@
color: #CCC;
}
.floater {
float: right;
margin-top: 16px;
.userHighlightCl {
padding: 2px 10px;
}
.userHighlightSel,
.userHighlightSel.select {
padding-top: 0;
padding-bottom: 0;
}
.userHighlightSel.select i {
line-height: 22px;
}
.userHighlightText {
width: 70px;
}
.userHighlightCl,
.userHighlightText,
.userHighlightSel,
.userHighlightSel.select {
height: 22px;
vertical-align: top;
margin-right: 0
}
}
</style>

View file

@ -1,14 +1,16 @@
<template>
<div class="user-finder-container">
<i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
<a href="#" v-if="hidden" :title="$t('finder.find_user')"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
<template v-else>
<input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
<button class="btn search-button" @click="findUser(username)">
<i class="icon-search"/>
</button>
<i class="button-icon icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
</template>
<div>
<div class="user-finder-container">
<i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" />
<a href="#" v-if="hidden" :title="$t('finder.find_user')"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden" /></a>
<template v-else>
<input class="user-finder-input" @keyup.enter="findUser(username)" v-model="username" :placeholder="$t('finder.find_user')" id="user-finder-input" type="text"/>
<button class="btn search-button" @click="findUser(username)">
<i class="icon-search"/>
</button>
<i class="button-icon icon-cancel user-finder-icon" @click.prevent.stop="toggleHidden"/>
</template>
</div>
</div>
</template>

View file

@ -3,7 +3,6 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
const UserPanel = {
props: [ 'activatePanel' ],
computed: {
user () { return this.$store.state.users.currentUser }
},

View file

@ -1,7 +1,7 @@
<template>
<div class="user-panel">
<div v-if='user' class="panel panel-default" style="overflow: visible;">
<user-card-content :activatePanel="activatePanel" :user="user" :switcher="false" :hideBio="true"></user-card-content>
<user-card-content :user="user" :switcher="false" :hideBio="true"></user-card-content>
<div class="panel-footer">
<post-status-form v-if='user'></post-status-form>
</div>

View file

@ -6,7 +6,7 @@ const UserProfile = {
created () {
this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.dispatch('startFetching', ['user', this.fetchBy])
if (!this.user) {
if (!this.user.id) {
this.$store.dispatch('fetchUser', this.fetchBy)
}
},
@ -29,14 +29,20 @@ const UserProfile = {
followers () {
return this.user.followers
},
userInStore () {
if (this.isExternal) {
return this.$store.getters.userById(this.userId)
}
return this.$store.getters.userByName(this.userName)
},
user () {
if (this.timeline.statuses[0]) {
return this.timeline.statuses[0].user
} else {
return Object.values(this.$store.state.users.usersObject).filter(user => {
return (this.isExternal ? user.id === this.userId : user.screen_name === this.userName)
})[0] || {}
}
if (this.userInStore) {
return this.userInStore
}
return {}
},
fetchBy () {
return this.isExternal ? this.userId : this.userName

View file

@ -43,7 +43,7 @@
flex: 2;
flex-basis: 500px;
.panel-heading {
.profile-panel-background .panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;

View file

@ -9,6 +9,7 @@ const userSearch = {
],
data () {
return {
username: '',
users: []
}
},
@ -21,7 +22,14 @@ const userSearch = {
}
},
methods: {
newQuery (query) {
this.$router.push({ name: 'user-search', query: { query } })
},
search (query) {
if (!query) {
this.users = []
return
}
userSearchApi.search({query, store: this.$store})
.then((res) => {
this.users = res

View file

@ -3,6 +3,12 @@
<div class="panel-heading">
{{$t('nav.user_search')}}
</div>
<div class="user-search-input-container">
<input class="user-finder-input" @keyup.enter="newQuery(username)" v-model="username" :placeholder="$t('finder.find_user')"/>
<button class="btn search-button" @click="newQuery(username)">
<i class="icon-search"/>
</button>
</div>
<div class="panel-body">
<user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card>
</div>
@ -10,3 +16,15 @@
</template>
<script src="./user_search.js"></script>
<style lang="scss">
.user-search-input-container {
margin: 0.5em;
display: flex;
justify-content: center;
.search-button {
margin-left: 0.5em;
}
}
</style>

View file

@ -257,7 +257,7 @@ const UserSettings = {
.then((res) => {
if (res.status === 'success') {
this.$store.dispatch('logout')
this.$router.push('/~/main/all')
this.$router.push({name: 'root'})
} else {
this.deleteAccountError = res.error
}

View file

@ -1,63 +1,34 @@
import apiService from '../../services/api/api.service.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import _ from 'lodash'
function showWhoToFollow (panel, reply) {
var users = reply
var cn
var index
var step = 7
cn = Math.floor(Math.random() * step)
for (index = 0; index < 3; index++) {
var user
user = users[cn]
var img
if (user.avatar) {
img = user.avatar
} else {
img = '/images/avi.png'
}
var name = user.acct
if (index === 0) {
panel.img1 = img
panel.name1 = name
panel.$store.state.api.backendInteractor.externalProfile(name)
.then((externalUser) => {
if (!externalUser.error) {
panel.$store.commit('addNewUsers', [externalUser])
panel.id1 = externalUser.id
}
})
} else if (index === 1) {
panel.img2 = img
panel.name2 = name
panel.$store.state.api.backendInteractor.externalProfile(name)
.then((externalUser) => {
if (!externalUser.error) {
panel.$store.commit('addNewUsers', [externalUser])
panel.id2 = externalUser.id
}
})
} else if (index === 2) {
panel.img3 = img
panel.name3 = name
panel.$store.state.api.backendInteractor.externalProfile(name)
.then((externalUser) => {
if (!externalUser.error) {
panel.$store.commit('addNewUsers', [externalUser])
panel.id3 = externalUser.id
}
})
}
cn = (cn + step) % users.length
}
_.shuffle(reply)
panel.usersToFollow.forEach((toFollow, index) => {
let user = reply[index]
let img = user.avatar || '/images/avi.png'
let name = user.acct
toFollow.img = img
toFollow.name = name
panel.$store.state.api.backendInteractor.externalProfile(name)
.then((externalUser) => {
if (!externalUser.error) {
panel.$store.commit('addNewUsers', [externalUser])
toFollow.id = externalUser.id
}
})
})
}
function getWhoToFollow (panel) {
var credentials = panel.$store.state.users.currentUser.credentials
if (credentials) {
panel.name1 = 'Loading...'
panel.name2 = 'Loading...'
panel.name3 = 'Loading...'
panel.usersToFollow.forEach(toFollow => {
toFollow.name = 'Loading...'
})
apiService.suggestions({credentials: credentials})
.then((reply) => {
showWhoToFollow(panel, reply)
@ -67,27 +38,24 @@ function getWhoToFollow (panel) {
const WhoToFollowPanel = {
data: () => ({
img1: '/images/avi.png',
name1: '',
id1: 0,
img2: '/images/avi.png',
name2: '',
id2: 0,
img3: '/images/avi.png',
name3: '',
id3: 0
usersToFollow: new Array(3).fill().map(x => (
{
img: '/images/avi.png',
name: '',
id: 0
}
))
}),
computed: {
user: function () {
return this.$store.state.users.currentUser.screen_name
},
moreUrl: function () {
var host = window.location.hostname
var user = this.user
var suggestionsWeb = this.$store.state.instance.suggestionsWeb
var url
url = suggestionsWeb.replace(/{{host}}/g, encodeURIComponent(host))
url = url.replace(/{{user}}/g, encodeURIComponent(user))
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 () {
@ -96,7 +64,7 @@ const WhoToFollowPanel = {
},
methods: {
userProfileLink (id, name) {
return generateProfileLink(id, name)
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
}
},
watch: {

View file

@ -7,12 +7,13 @@
</div>
</div>
<div class="panel-body who-to-follow">
<p>
<img v-bind:src="img1"/> <router-link :to="userProfileLink(id1, name1)">{{ name1 }}</router-link><br>
<img v-bind:src="img2"/> <router-link :to="userProfileLink(id2, name2)">{{ name2 }}</router-link><br>
<img v-bind:src="img3"/> <router-link :to="userProfileLink(id3, name3)">{{ name3 }}</router-link><br>
<img v-bind:src="$store.state.instance.logo"> <a v-bind:href="moreUrl" target="_blank">{{$t('who_to_follow.more')}}</a>
</p>
<span v-for="user in usersToFollow">
<img v-bind:src="user.img" />
<router-link v-bind:to="userProfileLink(user.id, user.name)">
{{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>
</div>
</div>
</div>
@ -28,7 +29,9 @@
width: 32px;
height: 32px;
}
.who-to-follow p {
.who-to-follow {
padding: 0.5em 1em 0.5em 1em;
margin: 0px;
line-height: 40px;
white-space: nowrap;
overflow: hidden;