merge develop, add mobile nav component

This commit is contained in:
shpuld 2019-03-12 23:50:54 +02:00
commit 7ce8fe9214
67 changed files with 2219 additions and 697 deletions

View file

@ -88,7 +88,7 @@
.attachment {
position: relative;
margin: 0.5em 0.5em 0em 0em;
margin-top: 0.5em;
align-self: flex-start;
line-height: 0;
@ -160,6 +160,7 @@
.hider {
position: absolute;
right: 0;
white-space: nowrap;
margin: 10px;
padding: 5px;

View file

@ -1,4 +1,4 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserCard from '../user_card/user_card.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -12,7 +12,7 @@ const BasicUserCard = {
}
},
components: {
UserCardContent,
UserCard,
UserAvatar
},
methods: {

View file

@ -1,18 +1,18 @@
<template>
<div class="user-card">
<div class="basic-user-card">
<router-link :to="userProfileLink(user)">
<UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
</router-link>
<div class="user-card-expanded-content" v-if="userExpanded">
<user-card-content :user="user" :switcher="false"></user-card-content>
<div class="basic-user-card-expanded-content" v-if="userExpanded">
<UserCard :user="user" :rounded="true" :bordered="true"/>
</div>
<div class="user-card-collapsed-content" v-else>
<div :title="user.name" class="user-card-user-name">
<div class="basic-user-card-collapsed-content" v-else>
<div :title="user.name" class="basic-user-card-user-name">
<span v-if="user.name_html" v-html="user.name_html"></span>
<span v-else>{{ user.name }}</span>
</div>
<div>
<router-link class="user-card-screen-name" :to="userProfileLink(user)">
<router-link class="basic-user-card-screen-name" :to="userProfileLink(user)">
@{{user.screen_name}}
</router-link>
</div>
@ -26,15 +26,15 @@
<style lang="scss">
@import '../../_variables.scss';
.user-card {
.basic-user-card {
display: flex;
flex: 1 0;
margin: 0;
padding-top: 0.6em;
padding-right: 1em;
padding-bottom: 0.6em;
padding-left: 1em;
border-bottom: 1px solid;
margin: 0;
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
@ -57,23 +57,6 @@
&-expanded-content {
flex: 1;
margin-left: 0.7em;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-width: 1px;
overflow: hidden;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
p {
margin-bottom: 0;
}
}
}
</style>

View file

@ -27,7 +27,6 @@
align-content: stretch;
flex-grow: 1;
margin-top: 0.5em;
margin-bottom: 0.25em;
.attachments, .attachment {
margin: 0 0.5em 0 0;
@ -36,6 +35,9 @@
box-sizing: border-box;
// to make failed images a bit more noticeable on chromium
min-width: 2em;
&:last-child {
margin: 0;
}
}
.image-attachment {

View file

@ -67,7 +67,7 @@ const ImageCropper = {
submit () {
this.submitting = true
this.avatarUploadError = null
this.submitHandler(this.cropper, this.filename)
this.submitHandler(this.cropper, this.file)
.then(() => this.destroy())
.catch((err) => {
this.submitError = err
@ -88,14 +88,14 @@ const ImageCropper = {
readFile () {
const fileInput = this.$refs.input
if (fileInput.files != null && fileInput.files[0] != null) {
this.file = fileInput.files[0]
let reader = new window.FileReader()
reader.onload = (e) => {
this.dataUrl = e.target.result
this.$emit('open')
}
reader.readAsDataURL(fileInput.files[0])
this.filename = fileInput.files[0].name || 'unknown'
this.$emit('changed', fileInput.files[0], reader)
reader.readAsDataURL(this.file)
this.$emit('changed', this.file, reader)
}
},
clearError () {

View file

@ -23,10 +23,7 @@
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;
margin-top: 0.5em;
.card-image {
flex-shrink: 0;

View file

@ -1,5 +1,5 @@
<template>
<div class="modal-view" v-if="showing" @click.prevent="hide">
<div class="modal-view media-modal-view" v-if="showing" @click.prevent="hide">
<img class="modal-image" v-if="type === 'image'" :src="currentMedia.url"></img>
<VideoAttachment
class="modal-image"
@ -32,18 +32,7 @@
<style lang="scss">
@import '../../_variables.scss';
.modal-view {
z-index: 1000;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 0.5);
.media-modal-view {
&:hover {
.modal-view-button-arrow {
opacity: 0.75;

View file

@ -0,0 +1,35 @@
import SideDrawer from '../side_drawer/side_drawer.vue'
import Notifications from '../notifications/notifications.vue'
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
const MobileNav = {
components: {
SideDrawer,
Notifications
},
data: () => ({
notificationsOpen: false
}),
computed: {
unseenNotifications () {
return unseenNotificationsFromStore(this.$store)
},
unseenNotificationsCount () {
return this.unseenNotifications.length
},
sitename () { return this.$store.state.instance.name }
},
methods: {
toggleMobileSidebar () {
this.$refs.sideDrawer.toggleDrawer()
},
toggleMobileNotifications () {
this.notificationsOpen = !this.notificationsOpen
},
scrollToTop () {
window.scrollTo(0, 0)
}
}
}
export default MobileNav

View file

@ -0,0 +1,29 @@
<template>
<nav class='nav-bar container' @click="scrollToTop()" id="nav">
<div class='inner-nav'>
<div class='item'>
<a href="#" class="menu-button" @click.stop.prevent="toggleMobileSidebar()">
<i class="button-icon icon-menu"></i>
</a>
<router-link class="site-name" :to="{ name: 'root' }" active-class="home">{{sitename}}</router-link>
</div>
<div class='item right'>
<a href="#" class="menu-button" @click.stop.prevent="toggleMobileNotifications()">
<i class="button-icon icon-bell-alt"></i>
<div class="alert-dot" v-if="unseenNotificationsCount"></div>
</a>
</div>
</div>
<SideDrawer ref="sideDrawer" :logout="logout"/>
<div class="mobile-notifications" :class="{ 'closed': !notificationsOpen }">
<Notifications/>
</div>
</nav>
</template>
<script src="./mobile_nav.js"></script>
<style lang="scss">
</style>

View file

@ -0,0 +1,91 @@
import PostStatusForm from '../post_status_form/post_status_form.vue'
import { throttle } from 'lodash'
const MobilePostStatusModal = {
components: {
PostStatusForm
},
data () {
return {
hidden: false,
postFormOpen: false,
scrollingDown: false,
inputActive: false,
oldScrollPos: 0,
amountScrolled: 0
}
},
created () {
window.addEventListener('scroll', this.handleScroll)
window.addEventListener('resize', this.handleOSK)
},
destroyed () {
window.removeEventListener('scroll', this.handleScroll)
window.removeEventListener('resize', this.handleOSK)
},
computed: {
currentUser () {
return this.$store.state.users.currentUser
},
isHidden () {
return this.hidden || this.inputActive
}
},
methods: {
openPostForm () {
this.postFormOpen = true
this.hidden = true
const el = this.$el.querySelector('textarea')
this.$nextTick(function () {
el.focus()
})
},
closePostForm () {
this.postFormOpen = false
this.hidden = false
},
handleOSK () {
// This is a big hack: we're guessing from changed window sizes if the
// on-screen keyboard is active or not. This is only really important
// for phones in portrait mode and it's more important to show the button
// in normal scenarios on all phones, than it is to hide it when the
// keyboard is active.
// Guesswork based on https://www.mydevice.io/#compare-devices
// for example, iphone 4 and android phones from the same time period
const smallPhone = window.innerWidth < 350
const smallPhoneKbOpen = smallPhone && window.innerHeight < 345
const biggerPhone = !smallPhone && window.innerWidth < 450
const biggerPhoneKbOpen = biggerPhone && window.innerHeight < 560
if (smallPhoneKbOpen || biggerPhoneKbOpen) {
this.inputActive = true
} else {
this.inputActive = false
}
},
handleScroll: throttle(function () {
const scrollAmount = window.scrollY - this.oldScrollPos
const scrollingDown = scrollAmount > 0
if (scrollingDown !== this.scrollingDown) {
this.amountScrolled = 0
this.scrollingDown = scrollingDown
if (!scrollingDown) {
this.hidden = false
}
} else if (scrollingDown) {
this.amountScrolled += scrollAmount
if (this.amountScrolled > 100 && !this.hidden) {
this.hidden = true
}
}
this.oldScrollPos = window.scrollY
this.scrollingDown = scrollingDown
}, 100)
}
}
export default MobilePostStatusModal

View file

@ -0,0 +1,76 @@
<template>
<div v-if="currentUser">
<div
class="post-form-modal-view modal-view"
v-show="postFormOpen"
@click="closePostForm"
>
<div class="post-form-modal-panel panel" @click.stop="">
<div class="panel-heading">{{$t('post_status.new_status')}}</div>
<PostStatusForm class="panel-body" @posted="closePostForm"/>
</div>
</div>
<button
class="new-status-button"
:class="{ 'hidden': isHidden }"
@click="openPostForm"
>
<i class="icon-edit" />
</button>
</div>
</template>
<script src="./mobile_post_status_modal.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.post-form-modal-view {
max-height: 100%;
display: block;
}
.post-form-modal-panel {
flex-shrink: 0;
margin: 25% 0 4em 0;
width: 100%;
}
.new-status-button {
width: 5em;
height: 5em;
border-radius: 100%;
position: fixed;
bottom: 1.5em;
right: 1.5em;
// TODO: this needs its own color, it has to stand out enough and link color
// is not very optimal for this particular use.
background-color: $fallback--fg;
background-color: var(--btn, $fallback--fg);
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3), 0px 4px 6px rgba(0, 0, 0, 0.3);
z-index: 10;
transition: 0.35s transform;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
&.hidden {
transform: translateY(150%);
}
i {
font-size: 1.5em;
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
@media all and (min-width: 801px) {
.new-status-button {
display: none;
}
}
</style>

View file

@ -1,6 +1,6 @@
import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserCard from '../user_card/user_card.vue'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -13,7 +13,7 @@ const Notification = {
},
props: [ 'notification' ],
components: {
Status, UserAvatar, UserCardContent
Status, UserAvatar, UserCard
},
methods: {
toggleUserExpanded () {

View file

@ -5,9 +5,7 @@
<UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.action.user.profile_image_url_original"/>
</a>
<div class='notification-right'>
<div class="usercard notification-usercard" v-if="userExpanded">
<user-card-content :user="notification.action.user" :switcher="false"></user-card-content>
</div>
<UserCard :user="notification.action.user" :rounded="true" :bordered="true" v-if="userExpanded"/>
<span class="notification-details">
<div class="name-and-action">
<span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
@ -25,7 +23,11 @@
<small>{{$t('notifications.followed_you')}}</small>
</span>
</div>
<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>
<div class="timeago">
<router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
<timeago :since="notification.action.created_at" :auto-update="240"></timeago>
</router-link>
</div>
</span>
<div class="follow-text" v-if="notification.type === 'follow'">
<router-link :to="userProfileLink(notification.action.user)">

View file

@ -11,7 +11,8 @@ const Notifications = {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
notificationsFetcher.startFetching({ store, credentials })
const fetcherId = notificationsFetcher.startFetching({ store, credentials })
this.$store.commit('setNotificationFetcher', { fetcherId })
},
data () {
return {

View file

@ -45,10 +45,6 @@
}
}
.notification-usercard {
margin: 0;
}
.non-mention {
display: flex;
flex: 1;
@ -126,7 +122,7 @@
}
.timeago {
font-size: 12px;
margin-right: .2em;
}
.icon-retweet.lit {

View file

@ -171,6 +171,9 @@ const PostStatusForm = {
},
formattingOptionsEnabled () {
return this.$store.state.instance.formattingOptionsEnabled
},
postFormats () {
return this.$store.state.instance.postFormats || []
}
},
methods: {
@ -219,6 +222,9 @@ const PostStatusForm = {
this.highlighted = 0
}
},
onKeydown (e) {
e.stopPropagation()
},
setCaret ({target: {selectionStart}}) {
this.caret = selectionStart
},

View file

@ -20,6 +20,7 @@
ref="textarea"
@click="setCaret"
@keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control"
@keydown="onKeydown"
@keydown.down="cycleForward"
@keydown.up="cycleBackward"
@keydown.shift.tab="cycleBackward"
@ -30,15 +31,17 @@
@drop="fileDrop"
@dragover.prevent="fileDrag"
@input="resize"
@paste="paste">
@paste="paste"
:disabled="posting"
>
</textarea>
<div class="visibility-tray">
<span class="text-format" v-if="formattingOptionsEnabled">
<label for="post-content-type" class="select">
<select id="post-content-type" v-model="newStatus.contentType" class="form-control">
<option value="text/plain">{{$t('post_status.content_type.plain_text')}}</option>
<option value="text/html">HTML</option>
<option value="text/markdown">Markdown</option>
<option v-for="postFormat in postFormats" :key="postFormat" :value="postFormat">
{{$t(`post_status.content_type["${postFormat}"]`)}}
</option>
</select>
<i class="icon-down-open"></i>
</label>

View file

@ -93,6 +93,9 @@ const settings = {
currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice
},
postFormats () {
return this.$store.state.instance.postFormats || []
},
instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }
},
watch: {

View file

@ -105,17 +105,9 @@
{{$t('settings.post_status_content_type')}}
<label for="postContentType" class="select">
<select id="postContentType" v-model="postContentTypeLocal">
<option value="text/plain">
{{$t('settings.status_content_type_plain')}}
{{postContentTypeDefault == 'text/plain' ? $t('settings.instance_default_simple') : ''}}
</option>
<option value="text/html">
HTML
{{postContentTypeDefault == 'text/html' ? $t('settings.instance_default_simple') : ''}}
</option>
<option value="text/markdown">
Markdown
{{postContentTypeDefault == 'text/markdown' ? $t('settings.instance_default_simple') : ''}}
<option v-for="postFormat in postFormats" :key="postFormat" :value="postFormat">
{{$t(`post_status.content_type["${postFormat}"]`)}}
{{postContentTypeDefault === postFormat ? $t('settings.instance_default_simple') : ''}}
</option>
</select>
<i class="icon-down-open"/>

View file

@ -1,4 +1,4 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserCard from '../user_card/user_card.vue'
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
// TODO: separate touch gesture stuff into their own utils if more components want them
@ -12,7 +12,7 @@ const SideDrawer = {
closed: true,
touchCoord: [0, 0]
}),
components: { UserCardContent },
components: { UserCard },
computed: {
currentUser () {
return this.$store.state.users.currentUser

View file

@ -8,19 +8,14 @@
@touchmove="touchMove"
>
<div class="side-drawer-heading" @click="toggleDrawer">
<user-card-content :user="currentUser" :switcher="false" :hideBio="true" v-if="currentUser"/>
<UserCard :user="currentUser" :hideBio="true" v-if="currentUser"/>
<div class="side-drawer-logo-wrapper" v-else>
<img :src="logo"/>
<span>{{sitename}}</span>
</div>
</div>
<ul>
<li v-if="currentUser" @click="toggleDrawer">
<router-link :to="{ name: 'new-status', params: { username: currentUser.screen_name } }">
{{ $t("post_status.new_status") }}
</router-link>
</li>
<li v-else @click="toggleDrawer">
<li v-if="!currentUser" @click="toggleDrawer">
<router-link :to="{ name: 'login' }">
{{ $t("login.login") }}
</router-link>
@ -119,14 +114,14 @@
}
.side-drawer-container-open {
transition-delay: 0.0s;
transition-property: left;
transition: 0.35s;
transition-property: background-color;
background-color: rgba(0, 0, 0, 0.5);
}
.side-drawer-container-closed {
left: -100%;
transition-delay: 0.5s;
transition-property: left;
background-color: rgba(0, 0, 0, 0);
}
.side-drawer-click-outside {
@ -181,15 +176,6 @@
display: flex;
padding: 0;
margin: 0;
.profile-panel-background {
border-radius: 0;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
}
}
.side-drawer ul {

View file

@ -3,7 +3,7 @@ import FavoriteButton from '../favorite_button/favorite_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
import DeleteButton from '../delete_button/delete_button.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserCard from '../user_card/user_card.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue'
@ -23,7 +23,7 @@ const Status = {
'highlight',
'compact',
'replies',
'noReplyLinks',
'isPreview',
'noHeading',
'inlineExpanded'
],
@ -259,7 +259,7 @@ const Status = {
RetweetButton,
DeleteButton,
PostStatusForm,
UserCardContent,
UserCard,
UserAvatar,
Gallery,
LinkPreview

View file

@ -1,6 +1,6 @@
<template>
<div class="status-el" v-if="!hideStatus" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
<template v-if="muted && !noReplyLinks">
<template v-if="muted && !isPreview">
<div class="media status container muted">
<small>
<router-link :to="userProfileLink">
@ -13,7 +13,7 @@
</template>
<template v-else>
<div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
<UserAvatar v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
<UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
<div class="media-body faint">
<span class="user-name">
<router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/>
@ -31,57 +31,67 @@
</router-link>
</div>
<div class="status-body">
<div class="usercard media-body" v-if="userExpanded">
<user-card-content :user="status.user" :switcher="false"></user-card-content>
</div>
<div v-if="!noHeading" class="media-body container media-heading">
<div class="media-heading-left">
<div class="name-and-links">
<UserCard :user="status.user" :rounded="true" :bordered="true" class="status-usercard" v-if="userExpanded"/>
<div v-if="!noHeading" class="media-heading">
<div class="heading-name-row">
<div class="name-and-account-name">
<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 :to="userProfileLink">
{{status.user.screen_name}}
</router-link>
<span v-if="isReply" class="faint reply-info">
<i class="icon-right-open"></i>
<router-link :to="replyProfileLink">
{{replyToName}}
</router-link>
</span>
<a v-if="isReply && !noReplyLinks" href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)" :aria-label="$t('tool_tip.reply')">
<i class="button-icon icon-reply" @mouseenter="replyEnter(status.in_reply_to_status_id, $event)" @mouseout="replyLeave()"></i>
<router-link class="account-name" :to="userProfileLink">
{{status.user.screen_name}}
</router-link>
</div>
<span class="heading-right">
<router-link class="timeago faint-link" :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">
<i :class="visibilityIcon(status.visibility)" :title="status.visibility | capitalize"></i>
</div>
<a :href="status.external_url" target="_blank" v-if="!status.is_local && !isPreview" class="source_url" title="Source">
<i class="button-icon icon-link-ext-alt"></i>
</a>
<template v-if="expandable && !isPreview">
<a href="#" @click.prevent="toggleExpanded" title="Expand">
<i class="button-icon icon-plus-squared"></i>
</a>
</template>
<a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="button-icon icon-eye-off"></i></a>
</span>
</div>
<div class="heading-reply-row">
<div v-if="isReply" class="reply-to-and-accountname">
<a class="reply-to"
href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
:aria-label="$t('tool_tip.reply')"
@mouseenter.prevent.stop="replyEnter(status.in_reply_to_status_id, $event)"
@mouseleave.prevent.stop="replyLeave()"
>
<i class="button-icon icon-reply" v-if="!isPreview"></i>
<span class="faint-link reply-to-text">{{$t('status.reply_to')}}</span>
</a>
<router-link :to="replyProfileLink">
{{replyToName}}
</router-link>
<span class="faint replies-separator" v-if="replies && replies.length">
-
</span>
</div>
<h4 class="replies" v-if="inConversation && !noReplyLinks">
<small v-if="replies.length">Replies:</small>
<small class="reply-link" v-bind:key="reply.id" v-for="reply in replies">
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}}&nbsp;</a>
</small>
</h4>
</div>
<div class="media-heading-right">
<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">
<i :class="visibilityIcon(status.visibility)" :title="status.visibility | capitalize"></i>
<div class="replies" v-if="inConversation && !isPreview">
<span class="faint" v-if="replies && replies.length">{{$t('status.replies_list')}}</span>
<span class="reply-link faint" v-if="replies" v-for="reply in replies">
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}}</a>
</span>
</div>
<a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url" title="Source">
<i class="button-icon icon-link-ext-alt"></i>
</a>
<template v-if="expandable">
<a href="#" @click.prevent="toggleExpanded" title="Expand">
<i class="button-icon icon-plus-squared"></i>
</a>
</template>
<a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="button-icon icon-eye-off"></i></a>
</div>
</div>
<div v-if="showPreview" class="status-preview-container">
<status class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status>
<status class="status-preview" v-if="preview" :isPreview="true" :statusoid="preview" :compact=true></status>
<div class="status-preview status-preview-loading" v-else>
<i class="icon-spin4 animate-spin"></i>
</div>
@ -123,7 +133,7 @@
<link-preview :card="status.card" :size="attachmentSize" :nsfw="nsfwClickthrough" />
</div>
<div v-if="!noHeading && !noReplyLinks" class='status-actions media-body'>
<div v-if="!noHeading && !isPreview" class='status-actions media-body'>
<div v-if="loggedIn">
<a href="#" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')">
<i class="button-icon icon-reply" :class="{'icon-reply-active': replying}"></i>
@ -147,6 +157,8 @@
<style lang="scss">
@import '../../_variables.scss';
$status-margin: 0.75em;
.status-body {
flex: 1;
min-width: 0;
@ -202,13 +214,16 @@
}
}
.media-left {
margin-right: $status-margin;
}
.status-el {
hyphens: auto;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
border-left-width: 0px;
line-height: 18px;
min-width: 0;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
@ -229,22 +244,33 @@
.media-body {
flex: 1;
padding: 0;
margin: 0 0 0.25em 0.8em;
}
.usercard {
margin-bottom: .7em
.status-usercard {
margin-bottom: $status-margin;
}
.user-name {
white-space: nowrap;
font-size: 14px;
overflow: hidden;
flex-shrink: 0;
max-width: 85%;
font-weight: bold;
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
}
.media-heading {
flex-wrap: nowrap;
line-height: 18px;
}
.media-heading-left {
padding: 0;
vertical-align: bottom;
flex-basis: 100%;
margin-bottom: 0.5em;
a {
display: inline-block;
@ -254,83 +280,102 @@
small {
font-weight: lighter;
}
h4 {
white-space: nowrap;
font-size: 14px;
margin-right: 0.25em;
overflow: hidden;
text-overflow: ellipsis;
}
.name-and-links {
.heading-name-row {
padding: 0;
flex: 1 0;
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
line-height: 18px;
.name-and-account-name {
display: flex;
min-width: 0;
}
.user-name {
margin-right: .45em;
flex-shrink: 1;
margin-right: 0.4em;
overflow: hidden;
text-overflow: ellipsis;
}
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
.account-name {
min-width: 1.6em;
margin-right: 0.4em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1 1 0;
}
}
.links {
.heading-right {
display: flex;
flex-shrink: 0;
}
.timeago {
margin-right: 0.2em;
}
.heading-reply-row {
align-content: baseline;
font-size: 12px;
color: $fallback--link;
color: var(--link, $fallback--link);
line-height: 18px;
max-width: 100%;
display: flex;
flex-wrap: wrap;
align-items: stretch;
a {
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
& > span {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
& > a:last-child {
flex-shrink: 0;
}
.reply-to-and-accountname {
display: flex;
height: 18px;
margin-right: 0.5em;
overflow: hidden;
max-width: 100%;
.icon-reply {
transform: scaleX(-1);
}
}
.reply-info {
display: flex;
}
.reply-to {
display: flex;
}
.reply-to-text {
overflow: hidden;
text-overflow: ellipsis;
margin: 0 0.4em 0 0.2em;
}
.replies-separator {
margin-left: 0.4em;
}
.replies {
line-height: 16px;
}
.reply-link {
margin-right: 0.2em;
}
}
.media-heading-right {
display: inline-flex;
flex-shrink: 0;
flex-wrap: nowrap;
margin-left: .25em;
align-self: baseline;
.timeago {
margin-right: 0.2em;
line-height: 18px;
font-size: 12px;
align-self: last baseline;
display: flex;
flex-wrap: wrap;
& > * {
margin-right: 0.4em;
}
}
> * {
margin-left: 0.2em;
}
a:hover i {
color: $fallback--text;
color: var(--text, $fallback--text);
.reply-link {
height: 17px;
}
}
@ -366,14 +411,19 @@
}
.status-content {
margin-right: 0.5em;
font-family: var(--postFont, sans-serif);
line-height: 1.4em;
img, video {
max-width: 100%;
max-height: 400px;
vertical-align: middle;
object-fit: contain;
&.emoji {
width: 32px;
height: 32px;
}
}
blockquote {
@ -390,9 +440,11 @@
}
p {
margin: 0;
margin-top: 0.2em;
margin-bottom: 0.5em;
margin: 0 0 1em 0;
}
p:last-child {
margin: 0 0 0 0;
}
h1 {
@ -417,7 +469,7 @@
}
.retweet-info {
padding: 0.4em 0.6em 0 0.6em;
padding: 0.4em $status-margin;
margin: 0;
.avatar.still-image {
@ -488,10 +540,10 @@
.status-actions {
width: 100%;
display: flex;
margin-top: $status-margin;
div, favorite-button {
padding-top: 0.25em;
max-width: 6em;
max-width: 4em;
flex: 1;
}
}
@ -517,9 +569,9 @@
.status {
display: flex;
padding: 0.6em;
padding: $status-margin;
&.is-retweet {
padding-top: 0.1em;
padding-top: 0;
}
}

View file

@ -132,7 +132,9 @@ const Timeline = {
}
if (count > 0) {
// only 'stream' them when you're scrolled to the top
if (window.pageYOffset < 15 &&
const doc = document.documentElement
const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0)
if (top < 15 &&
!this.paused &&
!(this.unfocused && this.$store.state.config.pauseOnUnfocused)
) {

View file

@ -4,7 +4,7 @@ import { requestFollow, requestUnfollow } from '../../services/follow_manipulate
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
export default {
props: [ 'user', 'switcher', 'selected', 'hideBio' ],
props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered' ],
data () {
return {
followRequestInProgress: false,
@ -16,7 +16,14 @@ export default {
}
},
computed: {
headingStyle () {
classes () {
return [{
'user-card-rounded-t': this.rounded === 'top', // set border-top-left-radius and border-top-right-radius
'user-card-rounded': this.rounded === true, // set border-radius for all sides
'user-card-bordered': this.bordered === true // set border for all sides
}]
},
style () {
const color = this.$store.state.config.customTheme.colors
? this.$store.state.config.customTheme.colors.bg // v2
: this.$store.state.config.colors.bg // v1
@ -93,22 +100,30 @@ export default {
},
methods: {
followUser () {
const store = this.$store
this.followRequestInProgress = true
requestFollow(this.user, this.$store).then(({sent}) => {
requestFollow(this.user, store).then(({sent}) => {
this.followRequestInProgress = false
this.followRequestSent = sent
})
},
unfollowUser () {
const store = this.$store
this.followRequestInProgress = true
requestUnfollow(this.user, this.$store).then(() => {
requestUnfollow(this.user, store).then(() => {
this.followRequestInProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
})
},
blockUser () {
const store = this.$store
store.state.api.backendInteractor.blockUser(this.user.id)
.then((blockedUser) => store.commit('addNewUsers', [blockedUser]))
.then((blockedUser) => {
store.commit('addNewUsers', [blockedUser])
store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
store.commit('removeStatus', { timeline: 'public', userId: this.user.id })
store.commit('removeStatus', { timeline: 'publicAndExternal', userId: this.user.id })
})
},
unblockUser () {
const store = this.$store

View file

@ -1,6 +1,6 @@
<template>
<div id="heading" class="profile-panel-background" :style="headingStyle">
<div class="panel-heading text-center">
<div class="user-card" :class="classes" :style="style">
<div class="panel-heading">
<div class='user-info'>
<div class='container'>
<router-link :to="userProfileLink(user)">
@ -11,7 +11,7 @@
<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>
<i class="button-icon icon-pencil usersettings" :title="$t('tool_tip.user_settings')"></i>
</router-link>
<a :href="user.statusnet_profile_url" target="_blank" v-if="isOtherUser && !user.is_local">
<i class="icon-link-ext usersettings"></i>
@ -108,7 +108,7 @@
</div>
</div>
</div>
<div class="panel-body profile-panel-body" v-if="!hideBio">
<div class="panel-body" v-if="!hideBio">
<div v-if="!hideUserStatsLocal && switcher" class="user-counts">
<div class="user-count" v-on:click.prevent="setProfileView('statuses')">
<h5>{{ $t('user_card.statuses') }}</h5>
@ -123,40 +123,75 @@
<span>{{user.followers_count}}</span>
</div>
</div>
<p @click.prevent="linkClicked" v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p>
<p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p>
<p @click.prevent="linkClicked" v-if="!hideBio && user.description_html" class="user-card-bio" v-html="user.description_html"></p>
<p v-else-if="!hideBio" class="user-card-bio">{{ user.description }}</p>
</div>
</div>
</template>
<script src="./user_card_content.js"></script>
<script src="./user_card.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.profile-panel-background {
.user-card {
background-size: cover;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
overflow: hidden;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
.panel-heading {
padding: .5em 0;
text-align: center;
box-shadow: none;
background: transparent;
flex-direction: column;
align-items: stretch;
}
}
.profile-panel-body {
word-wrap: break-word;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%);
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
.panel-body {
word-wrap: break-word;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%);
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
}
.profile-bio {
p {
margin-bottom: 0;
}
&-bio {
text-align: center;
img {
object-fit: contain;
vertical-align: middle;
max-width: 100%;
max-height: 400px;
.emoji {
width: 32px;
height: 32px;
}
}
}
// Modifiers
&-rounded-t {
border-top-left-radius: $fallback--panelRadius;
border-top-left-radius: var(--panelRadius, $fallback--panelRadius);
border-top-right-radius: $fallback--panelRadius;
border-top-right-radius: var(--panelRadius, $fallback--panelRadius);
}
&-rounded {
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
}
&-bordered {
border-width: 1px;
border-style: solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
}
}
@ -222,6 +257,7 @@
overflow: hidden;
flex: 1 1 auto;
margin-right: 1em;
font-size: 15px;
img {
object-fit: contain;
@ -392,25 +428,4 @@
text-decoration: none;
}
}
.usercard {
width: fill-available;
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-width: 1px;
overflow: hidden;
.panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
p {
margin-bottom: 0;
}
}
</style>

View file

@ -1,6 +1,6 @@
import LoginForm from '../login_form/login_form.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserCard from '../user_card/user_card.vue'
const UserPanel = {
computed: {
@ -9,7 +9,7 @@ const UserPanel = {
components: {
LoginForm,
PostStatusForm,
UserCardContent
UserCard
}
}

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 :user="user" :switcher="false" :hideBio="true"></user-card-content>
<UserCard :user="user" :hideBio="true" rounded="top"/>
<div class="panel-footer">
<post-status-form v-if='user'></post-status-form>
</div>
@ -11,13 +11,3 @@
</template>
<script src="./user_panel.js"></script>
<style lang="scss">
.user-panel {
.profile-panel-background .panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
}
</style>

View file

@ -1,6 +1,6 @@
import { compose } from 'vue-compose'
import get from 'lodash/get'
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserCard from '../user_card/user_card.vue'
import FollowCard from '../follow_card/follow_card.vue'
import Timeline from '../timeline/timeline.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more'
@ -141,10 +141,13 @@ const UserProfile = {
}
this.cleanUp()
this.startUp()
},
$route () {
this.$refs.tabSwitcher.activateTab(0)()
}
},
components: {
UserCardContent,
UserCard,
Timeline,
FollowerList,
FriendList

View file

@ -1,12 +1,8 @@
<template>
<div>
<div v-if="user.id" class="user-profile panel panel-default">
<user-card-content
:user="user"
:switcher="true"
:selected="timeline.viewing"
/>
<tab-switcher :renderOnlyFocused="true">
<UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/>
<tab-switcher :renderOnlyFocused="true" ref="tabSwitcher">
<Timeline
:label="$t('user_card.statuses')"
:disabled="!user.statuses_count"
@ -64,11 +60,6 @@
flex: 2;
flex-basis: 500px;
.profile-panel-background .panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
.userlist-placeholder {
display: flex;
justify-content: center;

View file

@ -157,8 +157,8 @@ const UserSettings = {
}
reader.readAsDataURL(file)
},
submitAvatar (cropper) {
const img = cropper.getCroppedCanvas().toDataURL('image/jpeg')
submitAvatar (cropper, file) {
const img = cropper.getCroppedCanvas().toDataURL(file.type)
return this.$store.state.api.backendInteractor.updateAvatar({ params: { img } }).then((user) => {
if (!user.error) {
this.$store.commit('addNewUsers', [user])