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

* upstream/develop: (29 commits)
  update api services
  use native scrollbar
  refactor using List component
  remove transition css
  update api endpoint url
  rewrite checkbox component
  fix panel title overflow issue in mobile
  fix double scrollbar display bug in mobile
  prevent parent scroll
  use custom scrollbar
  modal style improvements
  Update promisedRequest helper to support json payload
  Improve mobile layout
  Add a css class to the checkbox indicator
  update copy
  update generic error message
  add translations
  add error message
  reset modal state if api request is completed
  add processing state and close modal after api request is completed
  ...
This commit is contained in:
Henry Jameson 2019-05-05 11:42:42 +03:00
commit 680cb7eda7
26 changed files with 552 additions and 77 deletions

View file

@ -1,10 +1,10 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
const AvatarList = {
props: ['avatars'],
props: ['users'],
computed: {
slicedAvatars () {
return this.avatars ? this.avatars.slice(0, 15) : []
slicedUsers () {
return this.users ? this.users.slice(0, 15) : []
}
},
components: {

View file

@ -1,7 +1,7 @@
<template>
<div class="avatars">
<div class="avatars-item" v-for="avatar in slicedAvatars">
<UserAvatar :src="avatar.profile_image_url" class="avatar-small" />
<div class="avatars-item" v-for="user in slicedUsers">
<UserAvatar :user="user" class="avatar-small" />
</div>
</div>
</template>

View file

@ -1,7 +1,11 @@
<template>
<div class="basic-user-card">
<router-link :to="userProfileLink(user)">
<UserAvatar class="avatar" @click.prevent.native="toggleUserExpanded" :src="user.profile_image_url"/>
<UserAvatar
class="avatar"
:user="user"
@click.prevent.native="toggleUserExpanded"
/>
</router-link>
<div class="basic-user-card-expanded-content" v-if="userExpanded">
<UserCard :user="user" :rounded="true" :bordered="true"/>

View file

@ -63,6 +63,11 @@ const MobileNav = {
},
markNotificationsAsSeen () {
this.$refs.notifications.markAsSeen()
},
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
if (this.$store.state.config.autoLoad && scrollTop + clientHeight >= scrollHeight) {
this.$refs.notifications.fetchOlderNotifications()
}
}
},
watch: {

View file

@ -1,25 +1,26 @@
<template>
<nav class='nav-bar container' id="nav">
<div class='mobile-inner-nav' @click="scrollToTop()">
<div class='item'>
<a href="#" class="mobile-nav-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>
<nav class='nav-bar container' id="nav">
<div class='mobile-inner-nav' @click="scrollToTop()">
<div class='item'>
<a href="#" class="mobile-nav-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 class="mobile-nav-button" v-if="currentUser" href="#" @click.stop.prevent="openMobileNotifications()">
<i class="button-icon icon-bell-alt"></i>
<div class="alert-dot" v-if="unseenNotificationsCount"></div>
</a>
</div>
</div>
<div class='item right'>
<a class="mobile-nav-button" v-if="currentUser" href="#" @click.stop.prevent="openMobileNotifications()">
<i class="button-icon icon-bell-alt"></i>
<div class="alert-dot" v-if="unseenNotificationsCount"></div>
</a>
</div>
</div>
<SideDrawer ref="sideDrawer" :logout="logout"/>
</nav>
<div v-if="currentUser"
class="mobile-notifications-drawer"
:class="{ 'closed': !notificationsOpen }"
@touchstart="notificationsTouchStart"
@touchmove="notificationsTouchMove"
@touchstart.stop="notificationsTouchStart"
@touchmove.stop="notificationsTouchMove"
>
<div class="mobile-notifications-header">
<span class="title">{{$t('notifications.notifications')}}</span>
@ -27,12 +28,13 @@
<i class="button-icon icon-cancel"/>
</a>
</div>
<div v-if="currentUser" class="mobile-notifications">
<div class="mobile-notifications" @scroll="onScroll">
<Notifications ref="notifications" noHeading="true"/>
</div>
</div>
<SideDrawer ref="sideDrawer" :logout="logout"/>
<MobilePostStatusModal />
</nav>
</div>
</template>
<script src="./mobile_nav.js"></script>
@ -79,6 +81,8 @@
transition-property: transform;
transition-duration: 0.25s;
transform: translateX(0);
z-index: 1001;
-webkit-overflow-scrolling: touch;
&.closed {
transform: translateX(100%);

View file

@ -7,7 +7,7 @@
</status>
<div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]" v-else>
<a class='avatar-container' :href="notification.from_profile.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
<UserAvatar :compact="true" :betterShadow="betterShadow" :src="notification.from_profile.profile_image_url_original" />
<UserAvatar :compact="true" :betterShadow="betterShadow" :user="notification.from_profile"/>
</a>
<div class='notification-right'>
<UserCard :user="getUser(notification)" :rounded="true" :bordered="true" v-if="userExpanded" />

View file

@ -52,6 +52,10 @@ const Notifications = {
this.$store.dispatch('markNotificationsAsSeen')
},
fetchOlderNotifications () {
if (this.loading) {
return
}
const store = this.$store
const credentials = store.state.users.currentUser.credentials
store.commit('setNotificationsLoading', { value: true })

View file

@ -263,13 +263,13 @@ const Status = {
}
return this.status.summary_html + '<br />' + this.status.statusnet_html
},
combinedFavsAndRepeatsAvatars () {
combinedFavsAndRepeatsUsers () {
// Use the status from the global status repository since favs and repeats are saved in it
const combinedAvatars = [].concat(
const combinedUsers = [].concat(
this.statusFromGlobalRepository.favoritedBy,
this.statusFromGlobalRepository.rebloggedBy
)
return uniqBy(combinedAvatars, 'id')
return uniqBy(combinedUsers, 'id')
}
},
components: {

View file

@ -13,7 +13,7 @@
</template>
<template v-else>
<div v-if="retweet && !noHeading && !inConversation" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
<UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
<UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :user="statusoid.user"/>
<div class="media-body faint">
<span class="user-name">
<router-link v-if="retweeterHtml" :to="retweeterProfileLink" v-html="retweeterHtml"/>
@ -27,7 +27,7 @@
<div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet && !inConversation }]" :style="[ userStyle ]" class="media status">
<div v-if="!noHeading" class="media-left">
<router-link :to="userProfileLink" @click.stop.prevent.capture.native="toggleUserExpanded">
<UserAvatar :compact="compact" :betterShadow="betterShadow" :src="status.user.profile_image_url_original"/>
<UserAvatar :compact="compact" :betterShadow="betterShadow" :user="status.user"/>
</router-link>
</div>
<div class="status-body">
@ -91,8 +91,13 @@
</div>
<div v-if="showPreview" class="status-preview-container">
<status class="status-preview" v-if="preview" :isPreview="true" :statusoid="preview" :compact=true></status>
<div class="status-preview status-preview-loading" v-else>
<status class="status-preview"
v-if="preview"
:isPreview="true"
:statusoid="preview"
:compact=true
/>
<div v-else class="status-preview status-preview-loading">
<i class="icon-spin4 animate-spin"></i>
</div>
</div>
@ -134,7 +139,7 @@
</div>
<transition name="fade">
<div class="favs-repeated-users" v-if="combinedFavsAndRepeatsAvatars.length > 0 && isFocused">
<div class="favs-repeated-users" v-if="combinedFavsAndRepeatsUsers.length > 0 && isFocused">
<div class="stats">
<div class="stat-count" v-if="statusFromGlobalRepository.rebloggedBy && statusFromGlobalRepository.rebloggedBy.length > 0">
<a class="stat-title">{{ $t('status.repeats') }}</a>
@ -145,7 +150,7 @@
<div class="stat-number">{{ statusFromGlobalRepository.favoritedBy.length }}</div>
</div>
<div class="avatar-row">
<AvatarList :avatars='combinedFavsAndRepeatsAvatars'></AvatarList>
<AvatarList :users="combinedFavsAndRepeatsUsers"></AvatarList>
</div>
</div>
</div>

View file

@ -2,7 +2,7 @@ import StillImage from '../still-image/still-image.vue'
const UserAvatar = {
props: [
'src',
'user',
'betterShadow',
'compact'
],

View file

@ -1,8 +1,10 @@
<template>
<StillImage
class="avatar"
:alt="user.screen_name"
:title="user.screen_name"
:src="user.profile_image_url_original"
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
:src="imgSrc"
:imageLoadError="imageLoadError"
/>
</template>

View file

@ -151,6 +151,9 @@ export default {
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
},
reportUser () {
this.$store.dispatch('openUserReportingModal', this.user.id)
}
}
}

View file

@ -4,7 +4,7 @@
<div class='user-info'>
<div class='container'>
<router-link :to="userProfileLink(user)">
<UserAvatar :betterShadow="betterShadow" :src="user.profile_image_url_original"/>
<UserAvatar :betterShadow="betterShadow" :user="user"/>
</router-link>
<div class="name-and-screen-name">
<div class="top-line">
@ -99,8 +99,14 @@
</button>
</span>
</div>
<ModerationTools :user='user' v-if='loggedIn.role === "admin"'>
</ModerationTools>
<div class='block' v-if='isOtherUser && loggedIn'>
<span>
<button @click="reportUser">
{{ $t('user_card.report') }}
</button>
</span>
</div>
<ModerationTools :user='user' v-if='loggedIn.role === "admin"'/>
</div>
</div>
</div>

View file

@ -0,0 +1,106 @@
import Status from '../status/status.vue'
import List from '../list/list.vue'
import Checkbox from '../checkbox/checkbox.vue'
const UserReportingModal = {
components: {
Status,
List,
Checkbox
},
data () {
return {
comment: '',
forward: false,
statusIdsToReport: [],
processing: false,
error: false
}
},
computed: {
isLoggedIn () {
return !!this.$store.state.users.currentUser
},
isOpen () {
return this.isLoggedIn && this.$store.state.reports.modalActivated
},
userId () {
return this.$store.state.reports.userId
},
user () {
return this.$store.getters.findUser(this.userId)
},
remoteInstance () {
return !this.user.is_local && this.user.screen_name.substr(this.user.screen_name.indexOf('@') + 1)
},
statuses () {
return this.$store.state.reports.statuses
}
},
watch: {
userId: 'resetState'
},
methods: {
resetState () {
// Reset state
this.comment = ''
this.forward = false
this.statusIdsToReport = []
this.processing = false
this.error = false
},
closeModal () {
this.$store.dispatch('closeUserReportingModal')
},
reportUser () {
this.processing = true
this.error = false
const params = {
userId: this.userId,
comment: this.comment,
forward: this.forward,
statusIds: this.statusIdsToReport
}
this.$store.state.api.backendInteractor.reportUser(params)
.then(() => {
this.processing = false
this.resetState()
this.closeModal()
})
.catch(() => {
this.processing = false
this.error = true
})
},
clearError () {
this.error = false
},
isChecked (statusId) {
return this.statusIdsToReport.indexOf(statusId) !== -1
},
toggleStatus (checked, statusId) {
if (checked === this.isChecked(statusId)) {
return
}
if (checked) {
this.statusIdsToReport.push(statusId)
} else {
this.statusIdsToReport.splice(this.statusIdsToReport.indexOf(statusId), 1)
}
},
resize (e) {
const target = e.target || e
if (!(target instanceof window.Element)) { return }
// Auto is needed to make textbox shrink when removing lines
target.style.height = 'auto'
target.style.height = `${target.scrollHeight}px`
if (target.value === '') {
target.style.height = null
}
}
}
}
export default UserReportingModal

View file

@ -0,0 +1,157 @@
<template>
<div class="modal-view" @click="closeModal" v-if="isOpen">
<div class="user-reporting-panel panel" @click.stop="">
<div class="panel-heading">
<div class="title">{{$t('user_reporting.title', [user.screen_name])}}</div>
</div>
<div class="panel-body">
<div class="user-reporting-panel-left">
<div>
<p>{{$t('user_reporting.add_comment_description')}}</p>
<textarea
v-model="comment"
class="form-control"
:placeholder="$t('user_reporting.additional_comments')"
rows="1"
@input="resize"
/>
</div>
<div v-if="!user.is_local">
<p>{{$t('user_reporting.forward_description')}}</p>
<Checkbox v-model="forward">{{$t('user_reporting.forward_to', [remoteInstance])}}</Checkbox>
</div>
<div>
<button class="btn btn-default" @click="reportUser" :disabled="processing">{{$t('user_reporting.submit')}}</button>
<div class="alert error" v-if="error">
{{$t('user_reporting.generic_error')}}
</div>
</div>
</div>
<div class="user-reporting-panel-right">
<List :items="statuses">
<template slot="item" slot-scope="{item}">
<div class="status-fadein user-reporting-panel-sitem">
<Status :inConversation="false" :focused="false" :statusoid="item" />
<Checkbox :checked="isChecked(item.id)" @change="checked => toggleStatus(checked, item.id)" />
</div>
</template>
</List>
</div>
</div>
</div>
</div>
</template>
<script src="./user_reporting_modal.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.user-reporting-panel {
width: 90vw;
max-width: 700px;
min-height: 20vh;
max-height: 80vh;
.panel-heading {
.title {
text-align: center;
// TODO: Consider making these as default of panel
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.panel-body {
display: flex;
flex-direction: column-reverse;
border-top: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
overflow: hidden;
}
&-left {
padding: 1.1em 0.7em 0.7em;
line-height: 1.4em;
box-sizing: border-box;
> div {
margin-bottom: 1em;
&:last-child {
margin-bottom: 0;
}
}
p {
margin-top: 0;
}
textarea.form-control {
line-height: 16px;
resize: none;
overflow: hidden;
transition: min-height 200ms 100ms;
min-height: 44px;
width: 100%;
}
.btn {
min-width: 10em;
padding: 0 2em;
}
.alert {
margin: 1em 0 0 0;
line-height: 1.3em;
}
}
&-right {
display: flex;
flex-direction: column;
overflow-y: auto;
}
&-sitem {
display: flex;
justify-content: space-between;
> .status-el {
flex: 1;
}
> .checkbox {
margin: 0.75em;
}
}
@media all and (min-width: 801px) {
.panel-body {
flex-direction: row;
}
&-left {
width: 50%;
max-width: 320px;
border-right: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
padding: 1.1em;
> div {
margin-bottom: 2em;
}
}
&-right {
width: 50%;
flex: 1 1 auto;
margin-bottom: 12px;
}
}
}
</style>